高内聚和低耦合是很原则性、很“务虚”的概念。为了更好地将它们落地实践,我们有必要再多了解一些高内聚低耦合的度量标准。

这一篇与《细说几种内聚》是姊妹篇。可以对照着看。

耦合

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

图片

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.

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

内容耦合是耦合度最高的一种耦合。最常见、大概也是最可恶的例子,无疑就是Ctrl+C/Ctrl+V了。除此之外,不直接使用代码、但是重复实现功能,也可以算作内容耦合。

图片

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

在我们系统中有一个很重要的阈值,用户的某项分数必须达到这个阈值才能进行下一步操作。此外,这个阈值在多个系统中都被用到。

这本不成问题,可以说这本不该成为问题。然而这里偏偏就出了问题:这个简单的获取阈值的操作就在三个系统中、用不同的方式被重写了三次。系统A把阈值直接写死在代码中;系统B把阈值配置在本地数据库中;系统C把阈值配置在公共的配置系统中。

个中问题显而易见:如果我们要调整这个阈值,就必须通知三个系统一起调整。漏掉任一处,都会造成不可预估的后果。

这就是内容耦合带来的恶果:一次变更,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及其实现类却打破了这层封装,直接访问到了模块内部。

更糟糕的是,这个HappyNerYearVisitor还会根据不同的Relative实现类做不同处理,而这些处理很有可能要使用不同类的“私密”数据或者行为。例如我必须知道我姐有个没公开的男朋友才能“要挟”她、还得知道我哥打游戏“又菜又爱玩”才会向他挑战,等等。

这就是人们把这种情况称为“内容耦合”的原因。与Copy代码、重复实现一样,当这些“私密”内容发生变化时,影响范围必然会顺着“内容耦合”扩散到外部代码中。一旦外部代码“挂一漏万”了,后果不堪设想。 图片

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

眼尖的同学可能已经发现了:上面这个例子就是二十三种设计模式中的“访问者模式”。奇怪,设计模式不是很高上大的吗?为什么也会有这样内容耦合这样的强耦合呢?

这个事儿说来简单:任何设计——无论是架构设计、程序设计,还是建筑设计、平面设计——都是一个“取舍”的决策和过程。为了达到主要的设计目标,常常要舍弃一些次要目标。“访问者模式”就是这样一种取舍的结果。

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、然后找上门来兴师问罪。 图片

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

当然,不要直接访问其它系统的数据库已是程序员的共识,也是很少会犯的错误了。但是,公共耦合还会出现在其它场景下,例如我们有过这样一段代码:

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,以及所有字节流序列化/反序列化协议。在这些协议中,序列化与反序列化两方使用的数据结构必须完全一致:包名、类名、字段名、字段类型、乃至字段个数以及序列化版本号都必须完全一致。这就是一种典型的外部耦合:提供者与消费者共享一个外部强加的数据结构。

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;
    // 字段,略
}

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

图片

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。然而,这个系统很可能与def-api.jar毫无关联。其次,这两个jar包的版本还必须能匹配上。如果两个包的版本号没匹配上,就是熟悉的“NoSuchClassError/NoSuchMethodError”了。 图片

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

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

还有一种情况也可能产生特征耦合。当一个模块经历过多次扩展后,它的接口参数就可能变成这样:

public interface LoginService{
    void login(LoginDto dto);
}

public class LoginDto{
    /** 用户id */
    private long userId;
    /** 用户密码 */
    private String password;
    /** 用户名 */
    private String username;
    /** 手机号 */
    private String phoneNo;
    /** 手机验证码 */
    private String smsCode;
    /** 邮箱地址 */
    private String email;
    /** 刷脸图片地址 */
    private String faceUrl;
    /** 手势密码 */
    private String gesture;
    // 其它……
}

上面的代码中,LoginService几经迭代,已经扩展出了手机验证码登录、邮箱地址登录、刷脸登录、手势密码登录登多种服务。每一个新服务都会向接口入参LoginDto中增加一部分参数。同时,每一个登录服务都只会LoginDto中的一部分参数。在这些登录服务、以及登录服务的调用方之间,就出现了“共享一个数据结构、但是只使用了这个数据结构的一部分”的情况,也就是出现了特征耦合。

如果维持祖传代码一百年不动摇,这样的耦合似乎也无关紧要。无非就是让调用方摸不着头脑,不知道该传什么数据。但如果要修改祖传代码,这样的耦合可就让人头疼不已了:“这个字段你传了没有?我要改一下长度校验,你评估下对你的影响”,这样的话至少得重复一个月,还不一定能有明确的结论。

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

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内存管理都有帮助。不过这个问题以后再说,这里按下不表。

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

所以,要做到数据耦合,就应该“需要什么字段就传什么字段”,不多也不少。 图片

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

不过话说回来,完全使用基本类型来做参数传递有时会降低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”才能使用继承;继承应尽量遵循里氏替换原则;尽量用组合取代继承;等等。这都是我们必须慎之又慎的方面。 图片

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

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的形式暴露出来。曾经有位同学尝试用这种反射的方式来构建一个可扩展的功能模块,给我吓出一身冷汗…… 图片

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

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下标与正反面的对应关系)依赖于业务约束(用户先拍正面后拍反面),只要业务约束发生变化,系统逻辑就会遭殃,加班改bug就不可避免了。

诚然,系统逻辑多多少少都依赖于某些业务约束,也就是形式逻辑里所谓前置条件。但是,这类业务约束不应固化在代码逻辑中;实在不得已的,也应当越少越好、越宽松越好。这样,当业务需求发生变化时,系统逻辑才能够以不变应万变、或至少以小变应大变。开发才有可能从bug的沥青坑中抬起头来,去追寻自己的诗和远方。 图片

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

往期索引

《面向对象是什么》

从具体的语言和实现中抽离出来,面向对象思想究竟是什么?

《抽象》

抽象这个东西,说起来很抽象,其实很简单。

《高内聚与低耦合》

“高内聚”与“低耦合”是软件设计和开发中经常出现的一对概念。它们既是做好设计的方法论,也是评价设计好坏的标准。

《细说几种内聚》

高内聚和低耦合是很原则性、很“务虚”的概念。为了更好地将它们落地实践,我们有必要再多了解一些高内聚低耦合的度量标准。