细说几种内聚

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

    这里先说说几种内聚。
web


内聚数据库

    达到什么样的程度算高内聚?什么样的状况算低内聚?wiki上有一个内聚性的分类(https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion),咱们能够看看内聚都有哪些类型。设计模式


Coincidental cohesion:偶然内聚app

Coincidental cohesion is when parts of a module are grouped arbitrarily; the only relationship between the parts is that they have been grouped together (e.g., a “Utilities” class)ide

偶然内聚是指一个模块内的各个部分是很任性地组合到一块儿。偶然内聚的各个部分之间,除了“刚好放在同一个模块内”以外,没有任何关系。最典型例子就是“Utilities”类。性能

https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion


    这是内聚性最弱、也是最差的一种状况。这种状况下,应该尽可能把这个模块拆分红几个独立模块——即便如今不拆分,之后也早晚要拆。前阵子我就遇到了一个相似的问题。在咱们的系统中,有这样一个处理类:
优化


public interface UserBiz    UserBean queryUserBean(long userId);    UserInfo queryUserInfo(long userInfoId);    }


    乍一看,这个接口彷佛挺“高内聚”的。可是实际上,UserBean是从本地数据库中获取的、记录用户在当前业务线中的数据的类;而UserInfo是从用户中心获取的、记录用户注册信息数据的类:它们除了名字类似以外,基本没有相关性。把这两个数据的相关功能放在同一个模块中 ,就是一种“偶然内聚”。虽然在初期的使用中,这里并无什么问题。可是在后续扩展时,这种“偶然内聚”致使了循环依赖,咱们不得不把它们拆分红两个不一样的模块。this


Logical cohesion:逻辑内聚spa

Logical cohesion is when parts of a module are grouped because they are logically categorized to do the same thing even though they are different by nature .

逻辑内聚是指一个模块内的几个组件仅仅由于“逻辑类似”而被放到了一块儿——即便这几个组件本质上彻底不一样。

https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion


    不少文章里会特别指出,客户端每次调用逻辑内聚的模块时,须要给这个模块传递一个参数来肯定该模块应完成哪种功能 。这是由于逻辑内聚的几个组件之间并无什么本质上的类似之处,于是从入参提供的业务数据中没法判断应该按哪一种逻辑处理,而只好要求调用方额外传入一个参数来指定要使用哪一种逻辑。


    我早期作“可扩展”的设计时,常常会产生这种内聚。例如,有一个计算还款计划的接口,我是这样设计的:

public interface RepayPlanCalculator{    List<RepayPlan> calculate(LendApply apply,       CalculateParam param,       CalculateMethond calculateMethod);}


    除了借款申请和必要的计算参数(本金、期数、利率等)以外,这个接口还要求调用方传入一个计息方式字段,用以决定是使用等额本息、等额本金仍是其它公式计算利息。若是某天要增长一种计息方式,好比先息后本,也很好办:增长一种CalculateMethond就行。


    看起来一切都好,直到有一天业务要求停用等额本金方式,统一采用等额本息方式计算还款计划表。这时候咱们只有两种选择:要么让全部的调用方排查一遍本身调用这个接口时传入的参数,保证入参calculateMethod只传入了等额本息方式;或者,在接口内部作一个转换:调用方传入了等额本金方式,那么按等额本息方式处理。显然,第一种方式会把本来很小的一个需求变化扩散到整个系统中。这就好像只是被蚊子盯了一口却全身都长了大包同样。若是某一个调用方改漏了,那么它获得的还款计划表就是错的。若是这份错误的还款计划表到了用户手里,那么投诉扯皮事故复盘就少不了了。第二种方式则容易让调用方产生误解——明明指定了等额本金方式,为何计算结果是等额本息的?这就比如下单点了一份虾滑上菜给了一份黄瓜。若是这种误解一路传递给了用户——例如某个调用方的开发、产品一看参数支持等额本金,因而向用户宣传“咱们的产品支持等额本金”——那么投诉扯皮事故复盘就又要出现了。


    逻辑内聚也是一种“低内聚”,它把接口内部的逻辑处理暴露给了接口以外。这样,当这部分逻辑发生变动时,本来无辜的调用方就要受到牵连了。


Temporal cohesion:时间内聚

Temporal cohesion is when parts of a module are grouped by when they are processed - the parts are processed at a particular time in program execution

时间内聚是指一个模块内的多个组件除了要在程序执行到同一个时间点时作处理以外、没有其它关系。

https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion


    概念有点晦涩,举个例子就简单了:当Controller处理Http请求以前,用Filter统一作解密、验签、认证、鉴权、接口日志、异常处理等操做,那么,解密/验签/认证/鉴权/接口日志/异常吹了这些功能之间就产生了时间内聚。这些功能之间本来没有什么关系,可是考虑到这种时间内聚,咱们通常会把它们放到同一个包下、或者继承同一个父类。


/***  入参解密*/class DecodeFilter extends HttpFilter{    // 略}/***  入参验签*/class SignFilter extends HttpFilter{    // 略}/***  登陆认证*/class CertificationFilter extends HttpFilter{    // 略}// 其它相似,略


    这些操做、功能之间并无必然的联系——从这一点上来看,时间内聚也是一种弱内聚。但它多少仍是比偶然内聚和逻辑内聚要更强一些的:毕竟它们聚在一块儿是有正当理由的。就比如哪怕你都叫不全大学同班同窗的名字,但毕业十周年的时候聚一聚也是合情合理的。


Procedural cohesion:过程内聚

Procedural cohesion is when parts of a module are grouped because they always follow a certain sequence of execution.

过程内聚是指一个模块内的多个组件之间必须遵循必定的执行顺序才能完成一个完整功能。

https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion


    显然,过程内聚已是一种比较强的内聚了。存在过程内聚的几个功能组件应该尽量地放在一个模块内,不然在后续的维护、扩展中必定要吃苦头。


    在前面提到的那个金额计算的模块中,存在下面这种状况:


/** 计算器基类 */public abstract class Calculator{    private String formula;    protected Calculator(String formula){        super();        this.formula=fomrula;    }    public abstract CalculateResult calculate(CalculateParam money);}
/** 分期服务费计算器 */public class InstallmentServiceFeeCalculator exstends Calculator{    public ServiceFeeCalculator(){        // 分期服务费公式:分期本金*服务费费率        super("installmentPrincipal*serviceFeeRate");    }    /** 计算分期服务费 */    CalculateResult calculate(CalculateParam money){        // 注意:这里必须保证已经调用过InstallmentPricipalCalculator        // 并已经计算出了分期本金    }}/** 分期本金计算器 */public class InstallmentPricipalCalculator extends Calculator{    // 略}


    InstallmentServiceFeeCalculator是用来计算分期服务费的一个类。从分期服务费的计算公式能够看出:在计算分期服务费以前,必须先计算出分期本金。这样,InstallmentServiceFeeCalculator与InstallmentPricipalCalculator之间就有了过程耦合。应对这种状况,咱们有两种选择:一是让调用方在计算分期服务费以前,先本身计算一遍分期本金,而后把计算结果传给分期服务费计算器;二是让分期服务费计算器在必要的时候本身调用一次分期本金计算器。


    显然,第二种方式比第一种更好:分期服务费计算器和分期本金计算器之间存在过程耦合,第二种方式把它们放到了同一个模块内部。这样,不管哪一个计算器发生变化——修改公式、变动取值来源等——均可以只修改这个模块,而不会影响到调用方。


Communicational/informational cohesion:通讯内聚

Communicational cohesion is when parts of a module are grouped because they operate on the same data (e.g., a module which operates on the same record of information).

通讯内聚是指一个模块内的几个组件要操做同一个数据(例如同一个Dto、同一个文件、或者同一张表等)。

https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion


    对设计模式熟悉的同窗必定不会对通讯内聚感到陌生:责任链/代理等模式就是很典型的通讯内聚。例如,咱们曾有一个模块应该是这样的:


public interface DataCollector{    void collect(Data data);}class DataCollectorAsChain implements DataCollector{    private List<DataCollector> chain;    @Override    public void collect(Data data){        chain.foreach(collector-> collector.collect(data));    }}class DataCollectorFromServerA implements DataCollector{    @Override    public void collect(Data data){        // 从数据库里查到一堆数据        data.setDataA(xxx);    }}// 此外还有相似的从ServerB/ServerC的接口获取数据的几个类;// 这些类最终都会组合到DataCollectorAsChain的chain里面去。



    上面是一个典型的责任链模式。责任链上每一环都须要向Data中写入一部分数据,最终获得一个完整的Data。很显然,DataCollectorFromDb和DataCollectorFromRpc、DataCollectorFromHttp之间存在着通讯内聚,它们应该被放到同一个模块内。


    然而在咱们的系统中,这一条完整的责任链被完全拆散,零零碎碎地分布在业务流程的各个角落里;有些字段甚至被分散在了分布部署的好几个服务上。因而乎,咱们要查找某个字段取值问题时,总要翻遍整个流程才能肯定它到底在哪儿赋值、要如何修改;若是要增长字段、或者修改某些字段的数据来源,甚至要修改好几个系统的代码。这就是打破通讯内聚形成的恶果。


Sequential cohesion:顺序内聚


Sequential cohesion is when parts of a module are grouped because the output from one part is the input to another part like an assembly line.

顺序内聚是指在一个模块内的多个组件之间存在“一个组件的输出是下一个组件的输入”这种“流水线”的关系。

https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion


    若是熟悉Java8的Lambda表达式的话,应该很容易想到:Java8中的Stream就是一个顺序内聚的模块。例以下面这段代码中,从bankcCardList.stream()开启一个Stream以后,filter/map/map每一步操做的输出都是下一个操做的输入,并且它们必须按顺序执行,这正是标准的顺序内聚:


List<BankCard> bankCardList = ...;User u = ...;String bankCardPhone =     bankcCardList.stream()        .filter(card->card.no().equals(u.getBankCardNo())))        .map(BankCard::getPhone())        .map(phone -> "*******" + phone.subString(phone.lengh()-4)))        .orElse(StringUtils.EMPTY);



    除了Stream以外,设计模式中的装饰者/模板/适配器等模式也是很典型的顺序内聚……等等。例如,咱们来看这段代码:


public interface FlwoQueryService{    Optional<Flow> queryFlow(Queryer queryer);}class FlwoQueryServiceFromDbImpl{    public Optional<Flow> queryFlow(Queryer queryer){        // 从数据库里查询用户流程,略            }}abstract class FlowQueryServiceAsDecorator implements FlowQueryService{    private FlwoQueryService decorated;    public Optional<Flow> queryFlow(Queryer queryer){        // 装饰者,在decorated查询结果的基础上,作一次装饰处理        return decorated(queryer).map(flow-> decorate(flow, queryer));    }    /** 加强方法 */    protected abstract Flow decorate(Flow flow, Queryer queryer);}class FlowQueryServiceNotNullImpl extends FlowQueryServiceAsDecorator{    protected Flow decorate(Flow flow, Queryer queryer){        // 若是flow为null,则建立一个新数据    }}



    在上面的装饰者——固然也能够叫模板——类中,这两个步骤的顺序是固定的:必须先由被装饰者执行基础的查询操做、再由装饰者作一次加强操做;并且被装饰者的查询结果也偏偏就是装饰操做的一个入参。能够说,这段代码很完美的解释了什么叫“顺序内聚”。


    这段代码是咱们重构优化后的成果。在重构以前,咱们只有FlwoQueryServiceFromDbImpl。调用方须要本身判断和处理数据库中没有数据的状况,加上不一样业务场景下对没有数据的处理方式不一样,类似但不彻底相同的代码重复出现了好几回。所以,当处理逻辑发生变化——例如库表结构变了、或者字段取值逻辑变了时——咱们须要把全部引用的地方都检查一遍、而后再修改好几处代码。而在重构以后,全部处理逻辑都集中到了这个装饰者模块内,咱们能够很轻松地肯定影响范围、而后统一地修改代码。


Functional cohesion (best):功能内聚(最强内聚)

Functional cohesion is when parts of a module are grouped because they all contribute to a single well-defined task of the module .

功能内聚是指一个模块内全部组件共同完成一个功能、缺一不可。

https://en.wikipedia.org/wiki/Cohesion_(computer_science)#Types_of_cohesion


    功能内聚是最强的一种内聚。其它内聚更多的是在讨论把哪些组件组合成一个模块;而功能内聚的意义在于:它讨论的是把哪些组件提出当前模块。即便某个组件与模块内组件存在顺序内聚、通讯内聚、过程内聚,但只要这个组件与这个模块的功能无关,那这个组件就应该另谋高就。


    例如,咱们系统中有一个调用规则引擎的模块:


public interface CallRuleService{    RuleResult callRule(RuleData data);}class CallRuleService implements CallRuleService{    public RuleResult callRule(RuleData data){        validate(data);        RuleRequest request = transToRequest(data);        RuleResponse response = callRuleEngin(request);        return transToResult(response);    }}



    不管是校验、构建请求、调用引擎仍是解析结果,这个模块中全部的代码都是为了实现一个功能:调用规则引擎并解析结果。可是,随着业务发展、需求变动,这个模块中出现了愈来愈多的“噪音”:把调用规则引擎的request和response入库、在封装数据时把某个数据同步给某个系统、在获得响应后把某个字段发送给另外一个系统……诸如此类,不一而足。这些业务需求并不直接与“调用规则引擎”这个核心功能,相关组件与“调用核心规则”也只是顺序内聚(须要使用调用规则引擎的返回结果)、通讯内聚(须要使用调用规则引擎的入参/出参)甚至只是时间内聚(须要在调用规则引擎时同步数据)。从“功能内聚”的角度来看,这些新增代码就不该该放到这个模块中来。


    可是,因为一些历史缘由,这些代码、组件、需求全都被塞到了这个模块中。结果,这个模块不只代码很是臃肿,并且性能也十分低下:一次用户请求经常要20多秒才能完成,但是因为模块可维护和可扩展性差,重构优化也很是困难。若是当初能遵循“功能内聚”的要求,把没必要要的功能放到别的模块下,咱们也不会像如今这样望洋兴叹、无从下手了。


练习

    

    我在《高内聚与低耦合》文中举过一个这样的例子:

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


    这个模块中的组件属于哪一种内聚呢?


    严格一点说,右侧那些组件——从“提交信息”到“发送短信验证码”或“判断短信验证码是否正确”——属于功能内聚。它们全都是为了完成“短信签约”这个操做而组合到当前模块下的。


    可是,左侧这些组件——从“后续业务分发器”到“后续业务处理A”等——之间,只能算时间内聚。各类后续业务处理之间并无直接的、或者本质上的关联,它们被放在这个模块中的缘由仅仅是他们都要在短信签约完成以后作一些处理。这能够说是标准的时间内聚。


    左侧和右侧组件之间呢?从上面的分析也能看出来:这两大部分之间是顺序内聚。这个模块必须先调用右侧组件,在它们处理完成后才能去调用左侧组件进行处理。


    在《抽象》一文中,还有这样一个例子:


public interface CardListService{    List<Card> query(long userId, Scene scene);}//核心实现是这样的public class CardListServiceImpl{    private Map<Scene, CardListService> serviceMap;    public List<Card> query(long userId, Scene scene){        return serviceMap.get(scene).query(userId, scene);    }}// 返回字段是这样的public class Card{    // 客户端根据这个字段的值来判断当前银行卡是展现仍是置灰    private boolean enabled;    // 其它卡号、银行名等字段,和accessor略去}// 入参是这样的public enum Scene{    DEDUCT,    UN_BIND,    BIND;}



    在这个组件中,用于处理DEDUCT/UN_BIND/BIND等各类逻辑的组件之间是什么内聚关系呢?我认为是通讯内聚:它们都要针对入参userId和scene作处理,并返回一样的List<Card>。


qrcode?scene=10000004&size=102&__biz=MzUzNzk0NjI1NQ==&mid=2247484290&idx=1&sn=e09881b5faff35eb67536d2aefae800d&send_time=

相关文章
相关标签/搜索