语义耦合(Semantic Coupling)
跟小伙伴一起重构一段 UI,试图将用户界面和业务代码分离的时候,小伙伴试图在业务代码中直接调用 UI。我们当然都知道这会产生耦合,于是小伙伴试图定义一些属性、变量或接口来解决这个耦合。虽然在代码的静态分析中,这一的耦合消失了,但我始终觉得不妥。觉得耦合依然存在,只是不再能被静态分析了。
我想到一个词——“语义耦合(Semantic Coupling)”,搜索发现也有很多小伙伴在关心这个问题。而且,从他们的文章和讨论中,我也了解到更多关于语义耦合的种类和危害。
什么是语义耦合
这是区别于常规意义上的“耦合”而言的。
即类 Foo 依赖于类 Bar,即是常规意义上的耦合。静态代码分析工具就可以为我们发现这种耦合。如果将 Bar 拆开成两个部分,一是类 Bar 的实现本身,另一个是接口 IBar;现在 Foo 依赖的是接口 IBar,那么 Foo 就没有依赖类 Bar了。在静态代码分析工具中就会发现这样的依赖就解除了。
在静态代码分析工具认为没有耦合的情况之下,如果两个类之间还交换带有隐含意义的数据,假设对方已为自己完成了某种工作,暗示对方执行期望的代码,那么这两个类在语义上还存在着耦合。
我们说耦合的危害是修改一个类的时候,另一个类也需要做对应的修改。显式耦合有工具帮我们做重构时的解耦,而语义上的耦合却很难有准确帮助我们的工具。一些变态的工具(例如 ReSharper)能够帮助我们解决一部分。
哪些代码算作语义耦合
按照上面的定义,语义耦合的概念依然模糊,但都有一个统一的核心——在实现细节上存在依赖,而不是在调用上存在依赖。
交换带有隐含意义的数据
在这段代码中,Bar 依赖于 Foo,他们都依赖于 FooInfo。至少静态代码分析工具是这么认为的。
public class Foo
{
public void Do(object arg)
{
var info = (FooInfo) arg;
// 后续代码。
}
}
public class Bar
{
public void Test()
{
var info = new FooInfo();
_foo.Do(info);
}
}
但是,其实这里的 Foo 也依赖于 Bar(反向依赖),因为 Foo 总假设 Bar 一定传了一个 FooInfo 类型的参数。
在这里,Foo 对 Bar 的隐式依赖就构成了“语义耦合”。
如何消灭这段语义耦合呢?
将 object 类型的参数改为 FooInfo 类型是一个可选方案。但是,如果此函数是为了实现某个接口,object 是接口中对应方法的参数类型,那就不能这么改了。此时应该审视是否应该传入这个参数,或者审视接口设计的合理性。
假设对方已为自己完成了某种工作
典型的情况是要求调用某方法前先调用 Init。
public class Foo
{
private string _demo;
public void Init()
{
_demo = "walterlv";
}
public void Demo()
{
Console.WriteLine(_demo.Length);
}
}
public class Bar
{
public void Test()
{
var foo = new Foo();
foo.Init();
foo.Demo();
}
}
在这段代码中,如果 Bar 在使用 Demo 方法之前没有调用 Init,Foo 是会抛出异常的(事实上实现代码的异常不应该抛出,详情请参阅我的另一篇文章 永远不应该让实现异常抛出 - 吕毅)。类似的情况还有 Foo 中存在必须先赋值才能正常使用的字段/属性,或者必须按照特定的顺序调用才能正常实现的业务。
这里 Foo 便产生了对 Bar 语义上的耦合。虽然并没有明显的依赖,但几乎所有使用 Foo 的对象都要求要写成 Bar.Test() 里面的实现那样,否则用起来就不正常。
解决这里的语义耦合倒是有很多方法:
- 去掉
Init方法,改到构造函数中 - 将
Init改为普通的别的名称(比如InitializeXxx),然后让Demo方法允许在_demo为null时正常工作(并能解释为什么正常) - 如果初始化非常复杂必须在其他方法中实现,那么需要在
Demo方法的开头进行状态预判,并抛出异常说明必须先进行初始化(毕竟通过异常报告使用错误是强有力的文档,关于使用错误,请参阅我的另一篇文章 使用错误 - 吕毅)。
只有去掉 Init 方法才是真的解决了语义耦合,其他都是缓解语义耦合带来的危害。
暗示对方执行期望的代码
目前主流的 MVVM 框架几乎都支持 Message 机制,为了解决部分情况下 ViewModel 的操作需要通知到 View 来完成的情况。
这是一个好机制,因为它在框架层完成了 ViewModel 对 View 消息的传递,避免了 ViewModel 对 View 的依赖。
但是,这个机制太万能了,以至于各种不同的开发中可能写出实际上依然在耦合的代码(名义上已经不耦合了):
public class DemoView : IMessageReceiver<ShowErrorInfoMessage>, IMessageReceiver<DeleteAnimationMessage>
{
public void OnReceived(ShowIOErrorInfoMessage message)
{
// 弹窗显示 IO 错误。
}
public void OnReceived(DeleteAnimationMessage message)
{
// 播放某一项数据删除的动画。
}
}
public class DemoViewModel : ViewModelBase
{
private void Test()
{
try
{
// 执行某段业务代码。
SendMessage(new DeleteAnimationMessage(removingItemId));
// 继续执行某段业务代码。
}
catch(IOException ex)
{
SendMessage(new ShowIOErrorInfoMessage(ex));
}
}
}
在代码中,ViewModel 试图向 View 发送播放删除动画的消息和显示错误提示的消息,让 View 来播放动画并显示这些错误。
如果进行静态代码分析,ViewModel 依然对 View 没有任何依赖,但它们依然存在语义耦合。因为已经可以通过阅读代码来明白 ViewModel 正在试图播放动画和显示错误提示框。ViewModel正在期望对方来为自己实现某项自己无法单独实现的功能。
Message 毕竟是 MVVM 框架中一个强大的组成部分,只依赖于此机制也能够部分消除此耦合。方法是将 DeleteAnimationMessage 改名为 ItemRemovingMessage,将 ShowIOErrorInfoMessage 改名为 ErrorOccurredMessage。如此改动,那么 ViewModel 的代码中将不再包含任何期望 View 执行的逻辑,View 自己决定删除元素时是否播放动画(还是决定元素变灰),自己决定是否显示错误提示(还是决定自动纠正)。
这样的改动基本上没有语义耦合了,但我认为依然存在很弱的耦合,因为依然存在 ViewModel 试图期望 View 做某个任务,只是任务已经非常抽象了。
我在自己编写的 MVVM 框架中弱化了 Message 的机制(是非常的弱),逼迫 ViewModel 的实现者不要试图通知 View 做任何事情,而是由 View 的实现者决定是否对 ViewModel 中任务的执行结果进行反馈。
为什么语义耦合也有危害
直接的耦合可以在静态代码分析工具的帮助下帮助我们理清楚依赖关系并批量重构(重命名等),不过这个过程是非常痛苦的,尤其是耦合是双向的时候,或者被非常多类耦合的时候。
而语义上的耦合很难被静态代码分析工具分析出来,危害没有直接的耦合那么大,改起来也不那么痛苦。不过也有一些问题:
- 可能会隐藏着某些 BUG(尤其是在修改了被语义耦合的类时,根本就不知道对方会用怎样的方式在语义上耦合自己,改完还不一定出异常)
- 不利于单元测试(语义耦合会使得单元测试的用例变多,但可能根本就是无效或重复的;或者使得某些用例变得不可测,例如上面例子中要求单元测试播放动画或者显示错误提示框是不合理的)
- 设计上不那么好看(至少对强迫症患者来说是这样)
参考资料
- The Perils of Semantic Coupling - Wide Awake Developers
- Semantic coupling in code - Alejandro Duarte
语义耦合(Semantic Coupling)的更多相关文章
- 7.3 GRASP原则三: 低耦合 Low Coupling
3.GRASP原则三: 低耦合 Low Coupling How to support low dependency, low change impact and increased reuse? ...
- 语义分割(semantic segmentation) 常用神经网络介绍对比-FCN SegNet U-net DeconvNet,语义分割,简单来说就是给定一张图片,对图片中的每一个像素点进行分类;目标检测只有两类,目标和非目标,就是在一张图片中找到并用box标注出所有的目标.
from:https://blog.csdn.net/u012931582/article/details/70314859 2017年04月21日 14:54:10 阅读数:4369 前言 在这里, ...
- 语义分割Semantic Segmentation研究综述
语义分割和实例分割概念 语义分割:对图像中的每个像素都划分出对应的类别,实现像素级别的分类. 实例分割:目标是进行像素级别的分类,而且在具体类别的基础上区别不同的实例. 语义分割(Semantic S ...
- 【编译系统02】编译器 - 语义分析器(semantic)的简单设计思路(变量类与变量表)
当我们分析到 "int n;",说明其已经定义了一个变量,之后又遇到一个 "n=3",我们从哪里去找这个n并且赋值呢? 答案是:通过我们定义的 变量表(Tabl ...
- 04.从0实现一个JVM语言系列之语义分析器-Semantic
从0实现JVM语言之语义分析-Semantic 源码github, 如果这个系列文章对您有帮助, 希望获得您的一个star 本节相关语义分析package地址 致亲爱的读者: 个人的文字组织和写文章的 ...
- 【Unity Shader】二、顶点函数(vertex)和片元函数(fragment)传递数据,及各阶段可使用的语义(semantic)
学习资料:http://www.sikiedu.com/course/37/task/433/show 本节学习目标: 学习Shader中结构体struct的使用. 学习在片元函数(vertex)和顶 ...
- npm包的语义版本控制(Semantic Versioning of Packages)
本文删改自Node.js 8 the Right Way Part I Chapter 3 npm 使用语义版本控制(SemVer)来寻找包的最佳可用兼容版本. 以安装测试框架mocha为例 $ ...
- Architecture Pattern: Publish-subscribe Pattern
1. Brief 一直对Observer Pattern和Pub/Sub Pattern有所混淆,下面打算通过这两篇Blog来梳理这两种模式.若有纰漏请大家指正. 2. Role Publisher: ...
- 语义网 (Semantic Web)和 web 3.0
语义网=有意义的网络. "如果说 HTML 和 WEB 将整个在线文档变成了一本巨大的书,那么 RDF, schema, 和 inference languages 将会使世界上所有的数据变 ...
随机推荐
- 《用 Python 学微积分》笔记 3
<用 Python 学微积分>原文见参考资料 1. 16.优化 用一个给定边长 4 的正方形来折一个没有盖的纸盒,设纸盒的底部边长为 l,则纸盒的高为 (4-l)/2,那么纸盒的体积为: ...
- Office.资料
1.JAVA+JS如何在HTML页面上显示WORD文档内容?ActiveX只能兼容IE不考虑!_百度知道.html(https://zhidao.baidu.com/question/74594982 ...
- vue.js引用出错-script代码块放在head和body中的区别
这篇随笔是为了记录vue.js引用出错的原因,看到最后原来是vue.js代码放在head中不能正常使用,要最后发现要将其放在body中才行... 原来是js代码放在head和body中的区别问题,占个 ...
- 在网页链接中打开qq或者微信
打开微信: 先说第一种,大家知道,在自己的微信资料里有个二维码,别人扫描后可以查看你的资料添加你,把二维码扫描后,得到的地址是:http://weixin.qq.com/r/ykzexmzEPzFAr ...
- JSP 异常处理
JSP 异常处理 当编写JSP程序的时候,程序员可能会遗漏一些BUG,这些BUG可能会出现在程序的任何地方.JSP代码中通常有以下几类异常: 检查型异常:检查型异常就是一个典型的用户错误或者一个程序员 ...
- javascript的几种使用多行字符串的方式
JS里并没有标准的多行字符串的表示方法,但是在用模板的时候,为了保证模板的可阅读性,我们又不可避免的使用多行字符串,所以出现了各种搞法,这里以一段jade的模板作为示例,简单总结和对比一下. 字符串相 ...
- 设计模式--责任链模式C++实现
责任链模式C++实现 1定义 使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系.将这些对象链成一条链,并沿着这条链传递该请求/命令,直到有对象处理它为止 注:这里的请求.命令正 ...
- 9.深入理解AbstractQueuedSynchronizer(AQS)
1. AQS简介 在上一篇文章中我们对lock和AbstractQueuedSynchronizer(AQS)有了初步的认识.在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的 ...
- SOUI中启用拖文件
本文所用SOUI版本为1.0版本,在拖文件上与一般的消息略有不同. 1.添加拖文件消息响应 先与常规添加消息相同. class CMainFrm : public SHostWnd { public: ...
- css3 transform matrix矩阵的使用
Transform 执行顺序问题 — 后写先执行 matrix(a,b,c,d,e,f) 矩阵函数 •通过矩阵实现缩放 x轴缩放 a=x*a c=x*c e=x*e; y轴缩放 b= ...