细说几种耦合

    高内聚和低耦合是很原则性、很“务虚”的概念。为了更好的讨论具体技术,咱们有必要再多了解一些高内聚低耦合的度量标准。java

这一篇与《细说几种内聚》是姊妹篇。能够对照着看。程序员

花园的景昕,公众号:景昕的花园细说几种内聚


耦合web

    耦合性讨论的是模块与模块之间的关系。一样参考维基百科,咱们来看看耦合都有哪几种。面试

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1



Content coupling:内容耦合数据库

Content coupling is said to occur when one module uses the code of other module, for instance a branch. This violates information hiding - a basic design concept.编程

内容耦合是指一个模块直接使用另外一个模块的代码。这种耦合违反了信息隐藏这一基本的设计概念。json


    内容耦合是耦合度最高的一种耦合。最多见、大概也是最可恶的内容耦合,无疑就是Ctrl+C/Ctrl+V了。除此以外,不直接使用代码、可是重复实现功能,也能够算做内容耦合。设计模式

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1


ctrl+c/ctrl+v是面向搜(wa)索(keng)引(bu)擎(tian)编程的基本技能api


    例如,在咱们系统中有一个很重要的阈值,用户的某项分数必须达到这个阈值才能经过。并且这个阈值在多个系统中都要使用和判断处理。结果,这一个简单的获取阈值的操做就在三个系统中、用不一样的方式被重写了三次:系统A把阈值直接写死在代码中;系统B把阈值配置在本地数据库中;系统C把阈值配置在公共的配置系统中。问题是显而易见的:当产品要调整这个阈值时,他须要通知三个系统一块儿调整。这与咱们把一套代码Copy到N个地方的后果同样:一个变化,N处修改。数据结构


    有些文章会把“经过非正常入口而转入另外一个模块内部”也纳入内容耦合中。什么叫“转入一个模块内部”呢?咱们能够参考下面这个例子:



// 这个接口有诸多实现:Mother、Father、Sister、Brother、Uncle等,略public interface Relative {    // 接纳一个拜年的人    default void accept(HappNewYearVisitor visitor){        visitor.visit(this);    }}
// 这个接口定义了拜年的人public interface HappNewYearVisitor {   void visit(Mother mother);   void visit(Father father);   void visit(Sister sister);   void visit(Brother brother);   void visit(Uncle uncle);}// 我是这么拜年的public class Me implements HappNewYearVisitor{   public void visit(Mother mother){       // 给老妈发个打麻将红包   }   public void visit(Father father){       // 诈一下老爸的私房钱   }   public void visit(Sister sister){       // 妹儿~不给红包我就把你男友捅到爸妈那儿了哦   }   public void visit(Brother brother){       // 哥,来打一局游戏啊,谁输谁发红包   }   public void visit(Uncle uncle){       // 叔叔过年好   }}// 我堂妹是这么拜年的public class Cousin implements HappNewYearVisitor{   public void visit(Mother mother){       // 姆姆过年好   }   public void visit(Father father){       // 伯伯过年好   }   public void visit(Sister sister){       // 姐姐过年好,你的口红在哪买的好好看多少钱有代购吗……   }   public void visit(Brother brother){       // 哥哥过年好,红包呢红包呢红包呢红包呢红包呢红包呢红包呢红包呢   }   public void visit(Uncle uncle){       // 把把~~伦家今年想去苏州玩~~~~给发个旅游红包好不~~~~~~~~~~~~   }}



    上面这个例子中,Mother/Father/Sister/Brother/Uncle这些类,都处在Relative这个模块的内部。原则上,模块外部的任何类都只能经过接口来访问它们。可是,HappNewYearVisitor及其实现类却打破了这层封装,直接访问到了模块内部的具体类,并根据不一样的类作了不一样处理:这些不一样的处理颇有可能要使用不一样类的“私密”数据或者行为,例如我必须知道我姐有个没公开的男友才能“要挟”她、还得知道我哥虽然打游戏特别菜却恰恰不服输才会向他挑战,等等。这就是我把这种状况称为“内容耦合”的缘由。与Copy代码、重复实现同样,当这些“私密”内容发生变化时,与之耦合的代码必然也要发生变更。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

不知道说啥好,给您拜个早年吧


    眼尖的同窗可能已经发现了:上面这个例子就是二十三种设计模式中的“访问者模式”。可是,设计模式不是很高上大的吗?为何也会有这样内容耦合这样的强耦合呢?这个事儿说来简单:任何设计——不管是架构设计、程序设计,仍是建筑设计、平面设计——都是一个“取舍”的决策和过程:为了达到主要目标,经常要舍弃一些次要目标。“访问者模式”就是这样一种取舍的结果。


Common coupling:公共耦合

Common coupling is said to occur when several modules have access to the same global data. But it can lead to uncontrolled error propagation and unforeseen side-effects when changes are made.

公共耦合是指多个模块访问同一个全局数据。当(某个模块或全局数据)发生变化时,这种耦合可能会致使不受控制的错误传播以及没法预见的反作用。


    公共耦合也叫共享耦合、全局耦合。这个定义很容易让人联想到一些并发场景下的同步控制,例如信号量、生产者-消费者等:毕竟Java中的同步控制就是经过共享数据来实现的。不过,同步控制的组件通常都会放到同一个模块下,因此他们之间即便有公共耦合,问题也不大。


    容易出问题的是模块与模块之间、甚至是系统与系统之间的公共耦合。最多见的恐怕是系统A直接访问系统B的数据库表。咱们的系统目前就面临这样的问题:因为历史缘由,不少个外部系统直接访问了咱们的数据库表;尤为可怕的是,如今已经统计不清楚哪些系统访问了哪些表了。结果,虽然咱们正在大刀阔斧的对本身的系统进行重构优化,可是不只没法变动数据库表结构,甚至重构后的代码还得往已废弃的表里写入数据。不然,说不定哪一个系统就要出bug、而后找上门来兴师问罪。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

公共耦合的模块/系统之间,不知道何时就会爆发一场“私生子之战”。


    固然,不要直接访问其它系统的数据库基本上是程序员的共识、也是不多会再犯的错误了。可是,公共耦合还会出如今其它场景下,例如咱们有过这样一段代码:


public class XxxService{    private static final Bean BEAN = new Bean();    static{        // 初始化BEAN。Bean中全部get/set都是public的。BEAN.setA("a");        BEAN.setB("b");    }    public List<Bean> queryBeanList(){        // 先从数据库查一批数据       List<Bean> list = ...;       // 若是数据库么有数据,那么给个默认列表       if(Collections.isEmpty(list)){           list = Collections.singletonList(BEAN);       }       return list;    }}// 使用上面这个方法的类public class YyyService{    public void doSomething(){        List<Bean> beanList = xxxService.queryBeanList();        // 其它逻辑,略    }}public class ZzzService{    public void querySomeList(){        List<Bean> beanList = xxxService.queryBeanList();        // 其余逻辑,略    }}



    上面这段代码看起来没问题。可是,仔细梳理一下就能发现:XxxService和YyyService/ZzzService(以及任何调用了XxxService#queryBeanList()方法的模块)之间,在BEAN这个全局变量上产生了公共耦合。这种耦合会致使什么问题呢?一方面,若是XxxService变动了BEAN中的数据——也就是变动了默认列表的数据——那么YyyService/ZzzService等模块就有可能受到没必要要的牵连。另外一方面,若是YyyService模块修改了queryBeanList()的返回数据,那就有可能修改BEAN中的数据,从而在悄无声息间改变了queryBeanList()的逻辑、并致使ZzzService模块出现莫名其妙的bug。


    可见,公共耦合的耦合度也比较高,系统中应当尽可能避免出现这种耦合。


External coupling:外部耦合

External coupling occurs when two modules share an externally imposed data format, communication protocol, or device interface. This is basically related to the communication to external tools and devices.

外部耦合是指两个模块共享一个外部强加的数据结构、通讯协议或者设备接口。外部耦合基本上与外部工具和设备的通讯有关。


    说到外部耦合,我就想吐槽一下大名鼎鼎的Dubbo了。在Dubbo中,服务提供者与消费者通讯时使用的数据结构必须彻底一致:包名、类名、字段名、字段类型、乃至字段个数以及序列化版本号都必须彻底一致。这就是一种典型的外部耦合:提供者与消费者共享一个外部强加的数据结构。



public interface DubboFacade{    // 服务者与消费者使用的Request和Response必须彻底一致    Response call(Request req);}public class Request implements Serializable{    // 序列化版本号    private static final long serialVersionUID = -45245298751L;    // 字段,略}public class Response implements Serializable{    // 序列化版本号    private static final long serialVersionUID = -98639823124L;    // 字段,略}



    这种外部耦合就好像我必须有一个和你如出一辙的钱包才能找你借钱,只要样式、尺寸、纹路、甚至新旧程度上有一点点不同,我都借不到钱。它带来的问题也是显而易见的。当服务提供者要修改接口参数时,要么消费者所有随之升级;要么提供者维护多个版本——即便此次修改彻底能够向下兼容。而这两种方案在绝大多数状况下都是在给本身挖坑。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

java.lang.IllegalStateException: Serialized class Money must implement java.io.Serializable


    相似的问题在咱们本身的代码中也出现过。我在《抽象》一文中就举过这样一个例子。我参与设计过一套Java操做Excel文件的工具,底层用的是POI组件。


// 这是这个工具模块提供的接口public interface ExcelService<T>{    public List<T> read(HSSFWorkbook workBook);}// 调用方是这样的使用的public class DataService{    private ExcelService<Data> excelService;    public void parseData(){        // 读取一个excel文件       HSSFWorkbook workBook = ....;       // 把excel解析为数据封装类       List<Data> dataList = excelService.read(workBook);       // 后续处理,略    }}



    ExcelService这个工具的问题,在《抽象》一文中也已经提到过:它把Excel2003格式的组件HSSFWorkbook暴给了调用方,致使没法平滑升级更高版本的Excel。而致使这个问题的缘由,正是外部耦合:ExcelService和DataService这两个模块共享了一个外部的数据结构HSSFWorkbook。


Control coupling:控制耦合

Control coupling is one module controlling the flow of another, by passing it information on what to do (e.g., passing a what-to-do flag).

控制耦合是指一个模块经过传入一个“作什么”的数据来控制另外一个模块的流程。


    结合内聚类型来看,控制耦合对应的就是逻辑内聚。逻辑内聚的问题在前面已经讨论过,这里就再也不赘述了。



Stamp coupling:特征耦合

Stamp coupling occurs when modules share a composite data structure and use only parts of it, possibly different parts .

特征耦合是指多个模块共享一个数据结构、可是只使用了这个数据结构的一部分——可能各自使用了不一样的部分。


    特征耦合也叫数据结构耦合(data-structured coupling)。众所周知,面向对象编程很容易引起“类爆炸”问题,一段简简单单的逻辑中可能就要定义七八个类。要避免类爆炸问题,复用代码是一个不错的法子。可是,无脑地复用代码就有可能形成特征耦合,为后续的维护和扩展埋下隐患。


    例如,咱们有一个api包中的数据结构是这样定义的:


package com.abc.api.model;

import com.def.data.XxxInfoVO;import com.def.api.data.YyyVO;import com.def.api.data.ZzzInfoVO;
import java.io.Serializable;import java.util.List;

public class AbcVo implements Serializable {    private XxxInfoVO xxxInfo;    private YyyVO yyyInfo;    private ZzzInfoVO zzzInfo;}



    这里的问题比较隐蔽:AbcVo在com.abc的子包下;可是其成员变量xxxInfo/yyyInfo/zzzInfo倒是在com.def的子包下定义的。而在实际中,com.abc和com.def是由两个不一样的项目定义的两套api——假定分别是abc-api.jar和def-api.jar吧。这意味着什么呢?首先,某个系统若是要使用AbcVo,那不只须要引入abc-api.jar,还须要引入def-api.jar,而且这两个jar包的版本还必须能匹配上。若是两个包的版本号没匹配上,就是熟悉的“NoSuchClassError/NoSuchMethodError”了。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

我也不知道我依赖的这个包是否是我依赖的你依赖的这个包


    处理过各类框架的版本匹配问题的话,必定不会忘记被NoSuchClassError/NoSuchMethodError支配的恐惧。万万没想到咱们的业务代码中也埋着如此高级的雷。我该欣慰呢仍是难过呢。


    可是,也不要为了不特征耦合而走向另外一个极端。例如,信用卡和借记卡都是银行卡,不用为它们俩分别定义一个数据结构:



public class BankCardController{    public CardInfo queryCardInfo(Long userId){        // 分别查出放款卡和还款卡,略    }}public class CardInfo{    private CreditCardInfo creditCardInfo;    private DebitCardInfo debitCardInfo;}public class CreditCardInfo{    private String cardNo;    private String bankName;    private String userName;}public class DebitCardInfo{    private String cardNo;    private String bankName;    private String userName;}



Data coupling:数据耦合

Data coupling occurs when modules share data through, for example, parameters. Each datum is an elementary piece, and these are the only data shared (e.g., passing an integer to a function that computes a square root).

数据耦合是指模块间经过传递数值来共享数据。传递的每一个值都是基本数据,并且传递的值是就是要共享的值。


    数据耦合的耦合度很是低,模块间只经过数值传递耦合在一块儿。更况且这种数值传递还有两个附加条件:传递的每一个值都是基本数据;并且传递值就是要共享的值。为何要有这两个附加条件呢?


    首先,为何要求传递基本数据呢?通常来讲,与“基本数据”对应的是“指针”、“引用”、或者“复杂对象”这种数据。后者可能致使模块功能产生一些反作用。例如这种:


public class SomeService{    public void doSomething(Map<String, String> param){        param.put("a","abc");    }}



    上面这段代码看起来人畜无害。可是,假如某个调用方在调用doSomething方法时,传入的param中就已经有"a"="111"这个键值对了呢?在调用完这个方法后,param.get("a")不知不觉就编程了"abc"。这就有可能让调用方出现bug。若是doSomething方法把入参改成简单类型的值、而且"abc"做为返回值传递给调用方,就不会出现这个问题了。


    不过,“Java究竟是值传递仍是引用传递”这个问题也常常出现。这个问题其实颇有趣,对理解Java中的对象、引用甚至JVM内存管理都有帮助。不过这个问题之后再说,这里按下不表。


    至于为何要求传递的值就是要共享的值呢?简单的回答就是:若是传递了不须要使用的值,就会陷入特征耦合中。而特征耦合比数据耦合的耦合度更强。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

别说一百块,多一个字段都不给


    不过话说回来,彻底使用基本类型来作参数传递有时会下降API的可扩展性和可维护性。例如,考虑下面这个接口:


public interface IdCardService{    boolean checkIdNo(String idNo);}



    最第一版本中,这个服务只须要检查身份证号是否合法,而且返回结果就只有合法、不合法两种。可是,随着业务需求的发展,这个服务还要检查身份证号与姓名是否匹配;还要区分几种错误类型(身份证号错误,身份证号与姓名不匹配,等等)。这时,咱们只有两个办法:要么修改原有接口,要么增长多个方法。而这两种办法,都会像前面吐槽Dubbo时所说的那样各有弊端。


    可是,若是咱们一开始就用复杂对象来传递数据呢?像这样:


public interface IdCardService{    CheckResult checkIdNo(IdCard card);}public class IdCard{    // 最第一版本的字段    private String idNo;    // 随着需求发展而增长的字段    private String name;    private String address;   private Date startDate;   private Date endDate;}public class CheckResult{    // 最第一版本的字段    private boolean checkPass;    // 随着需求发展而增长的字段    private IdCardError error;}public enum IdCardError{    NO_ERROR,    ID_NUMBER_ILLEGAL,    NUMBER_NOT_MATCH_NAME,}



    这样定义接口可能产生特征耦合,可是其扩展性和维护性会更好一些。何去何从?这也是一种取舍。


Subclass coupling:子类耦合

Describes the relationship between a child and its parent. The child is connected to its parent, but the parent is not connected to the child.

子类耦合描述的是子类与父类之间的关系:子类连接到父类,可是父类并无连接到子类。


    子类耦合的耦合度很是高,我认为咱们能够把它看作是面向对象中的内容耦合:子类很是深的侵入到了父类的内部,而且能够经过重写来改变父类的行为。这也是为何虽然继承是面向对象的基本特性,可是面向对象设计并不提倡使用继承的一个缘由。


    使用继承带来的问题中,最典型的就是修改一个父类、影响全部子类。除此以外,子类对父类变量、方法的重写和覆盖也很容易带来问题——这类问题在各类面试题中都家常便饭;在咱们的系统中也偶有出现。例如,我曾经遇到过一段这样的代码:


/** 通用的返回结果定义 */public class CommonResult {    private boolean success;    private String message;    public boolean isSuccess() {        return this.success;    }    public void setSuccess(boolean success) {        this.success = success;    }}/** 某个接口自定义的返回结果 */public class SpecialResult extends CommonResult {    private Boolean success;    // 其它字段,略    public Boolean getSuccess() {        return this.success;    }    public void setSuccess(Boolean success) {        this.success = success;    }}



    用一个对象来封装全部API接口都要返回的公共字段、用它的子类来封装各接口特定的字段,这是一种比较通用的作法。上面的CommonResult和SpecialResult也是遵循这个思路来定义的。可是,SpecialResult做为子类,却错误地重写了父类中的成员变量和方法,致使这个接口在JSON序列化与反序列化时出了问题:


CommonResult o = new SpecialResult();o.setSuccess(true);// 猜猜这里的序列化结果是什么?System.out.println(new ObjectMapper().writeValueAsString(o));
String json = "{\"success\":true}";SpecialResult bo = new ObjectMapper().readValue(json, SpecialResult.class);// 猜猜这里的反序列化结果是什么?System.out.println(bo.isSuccess() + "," + bo.getSuccess());



    因为父子类之间的耦合度是如此之高,因此在使用继承时有诸多的约束:必须是“is-a”才能使用继承;继承应尽可能遵循里氏替换原则;尽可能用组合取代继承;等等。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

此次不放那个各类鸟的类图了……来出个戏,学习点鸟类学知识吧


Dynamic coupling:动态耦合


The goal of this type of coupling is to provide a run-time evaluation of a software system. It has been argued that static coupling metrics lose precision when dealing with an intensive use of dynamic binding or inheritance [4]. In the attempt to solve this issue, dynamic coupling measures have been taken into account.

动态耦合是用来衡量系统运行时的耦合状况的。有人认为,对于大量使用了动态绑定和继承的系统来讲,静态耦合不能很好地度量出模块间的耦合度。动态耦合就是为了解决这方面问题而提出的。



    动态绑定,例如继承、多态、反射、甚至序列化/反序列化等机制,确实给编码带来了很大的便利。可是动态一时爽……哈哈哈。动态耦合最大的问题在于:若是耦合的一方发生了变化,一般很难评估另外一方会受到什么影响——咱们甚至很难评估出哪些功能会受到影响,由于从静态的代码中很难监测到动态耦合的各方。例以下面这种代码:


SomeClass.getMethod("methondName").invoke("abc",String.class);
BeanUtils.copyProperties(dto, vo);
JsonUtils.fromJson(JsonUtils.toJson(dto), Vo.class);
Glass glass = (Glass)context.getBean("maybeGlassImpl");
<bean class="SomeService>    <property name="someDao" ref="someDaoImpl"/></bean>



    以第一种状况为例:若是SomeClass#methondName(String)方法的方法签名变了——例如扩展为SomeClass#methondName(String, int),这行代码可能不会有任何的错误提示。若是测试用例没有覆盖到这一行,那么这个问题就会被忽视掉,最后以线上bug的形式暴露出来。曾经有位同窗尝试用这种反射的方式来构建一个可扩展的功能模块,给我吓出一身冷汗……

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

我还找到了当时他画的设计图。上图中“统一处理类”就是用反射来作的。


Semantic coupling:语义耦合

This kind of coupling considers the conceptual similarities between software entities using, for example, comments and identifiers and relying on techniques such as Latent Semantic Indexing (LSI).

语义耦合是指两个软件实体使用了类似的概念——例如注释、标识符等等。(自动检测)语义耦合依赖于潜在语义索引(LIS)这类技术。


    语义耦合、潜在语义索引这些概念听着很高上大。我的理解,语义耦合说的就是系统逻辑依赖于业务约束。例如这种:


public interface IdCardService{    void dealIdCardPhoto(List<byte[]> photoList);}



    IdCardService这个接口的功能是对身份证正反面照片作处理。可是它的入参倒是一个List<byte[]>。这就带来一个问题:在这个List中,哪一个元素是身份证正面、哪一个是身份证反面?在咱们的系统中,photoList.get(0)是身份证正面,而photoList.get(1)是身份证反面。为何这样定义?由于按照业务需求,用户会先拍摄身份证正面照片、再拍摄身份证反面照片。


    显然地,这个接口会带来一个新的问题:若是业务需求变化,要求用户先拍摄身份证反面、后拍摄身份证正面呢?或者APP不强制要求顺序,用户能够本身决定先拍哪一面呢?或者哪怕需求没有发生变化,就是开发在后来的代码维护中修改了List中的顺序呢?因为这个接口内的系统逻辑(List下标与正反面的对应关系)依赖于业务约束(用户先拍正面后拍反面),当业务约束发生变化时,系统逻辑很容易就被殃及池鱼了。


    诚然,系统逻辑多多少少都依赖于某些业务约束,也就是形式逻辑里所谓前置条件。可是,这类业务约束应当越少越好、越宽松越好。这样,当业务需求发生变化时,系统逻辑才可以以不变应万变、或至少以小变应大变。

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

I have a dream:若是需求万变系统不变、或者需求大变系统小变(这话怎么这么别扭呢),开发是否是就不用这么加班了


qrcode?scene=10000004&size=102&__biz=MzUzNzk0NjI1NQ==&mid=2247484307&idx=1&sn=92c2952fe0a655a7fb36a287ca1dd619&send_time=

相关文章
相关标签/搜索