精华内容
下载资源
问答
  • 应用软件开发,就是对数据库进行增删改查操作?软件架构选型,就是选择几款流行的中间件?软件架构设计,就是把几个中间件串在一起?...本篇文章从应用软件的模块设计层面讲述软件设计的真正要求。

    应用软件开发,就是对数据库进行增删改查操作?软件架构选型,就是选择几款流行的中间件?软件架构设计,就是把几个中间件串在一起?如果真的这么认为,那么对应用系统设计可能还存在理解深度。本篇文章从应用软件的模块设计层面讲述软件设计的真正要求。

    功能模块拆分是在全面了解业务需求后,以寻找大量内聚性调用确定模块边界为目的,以寻求应用软件中易变变性和不易不变性的边界为目的应用系统设计过程。但是讨论功能模块拆分的前提是弄清楚什么是功能模块,然后才能讨论功能模块的拆分原则和设计方案

    1、功能模块的概念

    功能模块从业务层面的理解很简单,就是一个名词:如用户模块、订单模块、支付模块、合同模块;但隐含在这个名词下“功能模块”的技术意义,就至少需要具备以下特点:

    • 单一业务性:功能模块一定只处理单一业务,功能模块本身可能不能完成业务闭环,但业务闭环中关于某个业务点的处理,都应该由一个模块完成。

    • 闭合性和开放性:闭合性是指功能模块内部实现细节应该对外关闭,任何调用者不能进行修改。外部调用者要么使用要么不使用、要么直接使用要么整体替换;开放性是指业务模块本身的扩展是开放的,开发人员在不改变模块现有功能的情况下,可以对模块功能进行新增。

    • 抽象性:实际上该特点与上一个特点是紧密联系的,抽象性来源对业务需求的提取,通俗的来讲就是确定的业务边界有利于功能模块自身的扩展;例如车销订单和电商订单,虽然两种订单用于不同的业务闭环,但其业务的关键信息和对后续业务的驱动作用是一致的,所以两种类型的订单都应该归纳为一个订单模块。抽象性保证了功能模块的闭合性。

    • 接口规范性:接口规范性是指功能模块提供给外部调用者的接口调用方式、事件订阅方式是有边界的、稳定的。模块的接口可以进行规范且边界可控的原因,主要有赖于功能模块的单一业务性。如果开发人员发现功能模块提供给外部调用者的接口随时都在变化,那么说明模块的拆分存在问题。规范性同时保证了功能模块的开放性和闭合性。

    • 单向依赖的定位性:模块和外部模块的依赖一定是单向的,也就是说A模块如果直接或者间接依赖于B模块,那么B模块就一定不会“察觉”A模块的存在。由于依赖的单向性,所以模块在整个系统/子系统中一定可以找到清晰的层次定位。如果开发者发现模块无法在系统/子系统明确定位,那么说明模块的拆分存在问题。

    除了功能模块自身需具备的特点外,循环依赖问题也和功能模块的划分存在联系。那么什么叫循环依赖呢?循环依赖就是:两个或多个功能模块在接口层面出现相互依赖(包括直接和间接)的情况,例如岗位功能直接调用了用户功能的接口,用户功能在实现过程中又同时调用了岗位功能的接口(实际工作中,那些间接产生的循环调用,也会形成循环依赖),示例代码如下:

    // 岗位模块逻辑实现中依赖了用户模块的接口
    // ……
    public class PositionServiceImpl implements PositionService {
      @Autowired
      private UserService userService;
      public void doSomething() { }
    }
    // ============
    // 用户模块的逻辑实现中依赖了岗位模块的接口
    // ……
    public class UserServiceImpl implements UserService {
      @Autowired
      private PositionService positionService;
      public void doOtherthing() { }
    }
    

    循环依赖本身不是绝症,在编程技巧上来说循环依赖还可以减小功能逻辑的实现难度,提高单位时间内代码的编写效率(不需要关注设计模式的应用,只需要按照业务流程撸出代码)。但是如果将循环依赖状态和模块设计联系在一起,那么循环依赖将会对功能模块设计产生较大负面影响。

    简单来说,如果功能模块间出现循环依赖,那么功能模块就无法形成单向依赖,无法稳定在系统/子系统上的某个固定层级;另外,如果功能模块内部出现循环依赖,就代表这个功能模块无法继续向下进行更细粒度的拆分。
    在这里插入图片描述
    为什么模块间存在循环依赖就表明模块拆分失败呢?这是因为一旦存在循环依赖,将直接导致功能模块不满足单向依赖的特点要求,也就无法稳定存在于系统/子系统上的某个层级。也就是说,功能模块内部是否存在循环依赖是进行模块边界辨识的重要依据。边界以外的功能和本模块只存在标准的接口调用和事件订阅;边界以内的功能由于存在循环依赖,所以不能再继续向下进行更细粒度的拆分。如果边界以内的功能不存在循环依赖,那么说明模块还可以继续向下进行更细粒度的拆分(虽然不一定要这样做)。

    2、模块拆分原则

    2.1、高内聚性

    高内聚性用于描述模块内各功能的调用关系。高内聚性是指模块内所有接口、接口层级调用的紧密程度。这些被紧密集合在一起的工作逻辑对外是透明的,且只为一个目标而存在,就是从模块内所处不同层次出发,共同完成业务模块所负责的单一业务任务。

    例如,业务模块中的数据层只是为了完成和业务相关的数据的持久化存取存在的,不会在数据层去存取和另外业务相关的全部数据。业务模块中的业务逻辑层,只是为了完成和本业务逻辑相关的计算,其余周边业务的处理要么调用其它模块的接口完成,要么通过事件机制将自身处理情况通知出去,再由其它模块的订阅者负责完成……。
    在这里插入图片描述
    高内聚性在帮助开发人员提高开发效率的前提下,还可能导致循环依赖。循环依赖不一定完全是坏事,这要看技术团队对业务拆分的理解和要求。例如如果研发团队根据需求调研决定将用户、岗位、职位、职级归纳于一个模块,那么用户、岗位、职位、职级的逻辑关联实现就不必考虑避免四个功能需要单独形成四个模块的问题,也无需考虑循环依赖的问题(但实际情况来看这种模块粒度的设计显然太过粗放)。

    2.2、低耦合性

    耦合性用于描述模块和模块间的关联紧密程度。模块依赖的外部模块越多、需要关注的其它模块的事件越多,则模块的扩展难度越大、替换成本越高。从系统设计的角度来看,降低功能模块的耦合性比提高模块内的聚合性更为重要。这个原因很容易理解,模块内的聚合性是否紧密仅仅涉及到该模块本身设计的好坏,而模块间的低耦合性将保证模块内不好的设计所影响的范围被限制在模块内,而不会被传递到其它模块。

    2.2.1、为了达到低耦合性的要求,有几类模块间的关联方式是绝对需要避免的:

    • A、直接跨过其它模块的标准接口,对其它模块的数据进行读写:这个原因很好理解,这种修改方式将直接取决于其它模块的业务实现细节,如果其它模块的业务逻辑在内部被修改或者其他模块的实现方式直接被替换,那么本模块内的相关处理逻辑则不得不进行修改。这种处理方式也违背了面向对象设计的基本原则——依赖接口而非依赖实现。以下是一个错误示例:
      在这里插入图片描述
    • B、两个或多个模块都对同样的业务进行操作:这种场景常见于两个或者多个模块进行数据绑定的情况。例如,物流模块中货运单负责人和用户模块中人员进行绑定关联的操作场景。一个非常明显的问题是:到底应该由哪个业务模块来控制这个绑定关系?

    注意:本文一直讨论的是功能模块的设计问题,而不是用户UE交互问题。在UE层面,从方便用户操作的角度来看当然可以在用户模块提供一个直接绑定货运单的操作界面,但是在功能模块设计层面,对于绑定信息的操作当然不能设计成两个模块都可以管理绑定数据。正确的设计方式是,只能由上层模块完成绑定数据的维护(也就是归纳到上层功能模块中进行管理)。如下图所示:
    在这里插入图片描述
    这是为什么呢?首先讨论这个问题的前提是,功能模块的拆分满足要求(即上文以讨论过的功能模块应该具备的所有特性)。在这种情况下,具有这种数据绑定关系的功能模块一定具有单向依赖特点。

    将两个业务模块的绑定关系(特别是多对多关系)放置于上层模块,可以使下层模块减少关注规模、保证下层模块的稳定性,还可以增加上层模块的扩展性。例如如后续需要增加订单创建者的数据绑定关系,则无需修改用户模块,只需要增加新的订单模块。
    在这里插入图片描述
    如果设计者发现无法确定某种绑定关系不知道应该放置在哪个功能模块中,则最可能的原因是:这两个或者多个功能模块拆分失败,需要重新进行功能模块设计。或者这些关联数据是功能模块内具有内聚性的绑定信息。那么有的读者会问,如果将绑定关系交给上层模块维护,当时又需要在查询用户信息时一起关联出指定用户和物流单绑定,该怎么办呢?不要急,后续讲解如何进行模块拆分时会进行解决方式的讲解。

    2.2.2、另一种场景的模块间依赖方式,是应该尽可能减少或被限制的:

    将两个或者多个设计存在瑕疵的功能模块中存在循环依赖的部分提取出来下沉为一个诸如common-XXXX的公共功能模块。如下图所示:
    在这里插入图片描述
    这里的common模块当然是一种解决多个模块中依赖冲突的办法,但是作为下沉的功能模块,该模块存在一些问题:首先该模块一定会涉及应该由其它功能关注或操作的业务逻辑,所以该办法治标不能治本。而最根本的原因是这个办法将功能模块间的循环依赖问题迁移到这个公共模块的内部,让循环依赖以内聚性的方式继续存在,而不是解决这一系列循环依赖。

    另外,上层功能模块依赖该公共模块后,会将上层功能模块无需关注的接口、模型、事件暴露出来,增加上层模块的开发难度。最后,增加了一个无法归入任何特定功能模块的所谓公共功能模块,一定会增加业务系统本身的维护难度,而在后续的二次开发环节中,开发人员对于是否需要引入、修改这个公共模块一定会存在疑惑。

    2.2.3、好的低耦合设计将直接帮助系统设计达到一下几个效果:

    • A、更容易的二次开发实施
      这个效果用一个通俗易懂的方式进行描述,就是:好的功能模块可以在二次开发阶段由二次开发团队按照自己的需求思路和技术思路进行完全重构,且这样的二次开发是有明确边界的,这个边界应该和被二次开发替换模块的功能边界一致。二次开发团队在进行功能模块重写时,无需关注这个模块以外的模块工作原理,因为其他模块不会因为这次重写而发生改变。

    • B、功能模块层级明确
      在产品团队在进行前期产品设计或者对客户的售前工作中,经常会向团队或者目标客户出示产品的功能架构图,类似如下:
      在这里插入图片描述
      那么技术团队能否在产品/项目研发阶段真正按照这样给客户宣讲(忽悠)的功能架构图完成系统中各个功能模块的构建呢?答案是肯定的,只要按照上文提到的构建功能模块的基本原则进行系统设计,那么系统中的各个模块就可以呈现“漂亮”的顺序依赖结构。但很多时候,由于错误的功能模块拆分方式,技术团队往往无法达到产品前期设计的或者给客户承诺的功能模块拆分目标。

    • C、具有单向依赖特点和层次特点的功能模块,自身是稳定的

      这种稳定性体现在代码修改、替换的边界控制上。举个例子:当开发人员出于某些目的,需要将某个模块的实现代码删除/剪切(除暴露的接口),那么开发人员可以观察到的效果是当开发人员剪切了模块的所有代码到另外的地方,该模块本身不会报错;同样该模块原始存在的应用工程也不会报错。

      这种稳定性还体现在模块内错误的可控性——系统内的虫子被有效限制:系统研发过程中难免出现技战术问题和失误,例如边界校验问题、性能问题、需求理解问题、数值适配问题等等(bug)。但是由于好的功能模块设计的内聚性和隔离性,这些虫子活动范围只会限于各个功能模块内部。

    3、如何进行模块拆分

    那么如何进行功能模块的拆分呢?上文已经提到,功能模块拆分的原则是提搞功能模块的内聚性,降低功能模块间的耦合性。其中更重要的原则是降低模块间的耦合性,高内聚性的形成则降低模块间耦合性后的必然产物。

    3.1、基于不同业务场景,使用规范的设计模式,降低依赖:

    模块间的耦合可通过多种设计模式(主要是行为模式)进行降低(但需要注意,采用设计模式的最大原则是,同一类型问题采用相同的设计模式进行设计),最好各个模块只存在最少方法调用、最小对象传参这样的依赖方式,最小限度来说必须解决功能模块间的循环依赖问题。请看如下示例:岗位模块和用户模块由于设计问题被耦合在一起,两者存在循环依赖——这是两个坏的模块设计:

    // ……
    // 岗位模块逻辑实现中依赖了用户模块的接口
    public class PositionServiceImpl implements PositionService {
      @Autowired
      private UserService userService;
      public void doSomething() { }
    }
    // ==============
    // 用户模块的逻辑实现中依赖了岗位模块的接口
    // ……
    public class UserServiceImpl implements UserService {
      @Autowired
      private PositionService positionService;
      public void doOtherthing() { }
    }
    

    在没有解决循环依赖问题前,两个模块是分不出来业务层级的。如下图所示:
    在这里插入图片描述
    为了将两个模块进行最低限度的解耦,分出两个模块的层次,技术人员需要让两个模块的依赖关系变成单向的,如下图所示:
    在这里插入图片描述
    由于用户模块被其他功能模块调用的可能性要高于岗位模块,而且用户模块更需要进行抽象,所以一般认为用户模块应该位于岗位模块的下层(但这也不一定,决定于实际场景下的需求情况)。换句话说,岗位模块可以依赖用户模块,可以使用用户模块的SDK层(接口层)接口、模型;但是用户模块不应该依赖岗位模块,甚至不应该知晓用户模块上层的任何模块(包括岗位模块)的存在。

    从技战术的角度讲,我们只需要一些很简单的办法,就可以解决这个问题:即在用户模块提供事件通知,将用户模块工作逻辑中需要由上层模块协作完成的事件触发点公布出去,然后由上层模块按照相关需求进行实现即可。这个过程可以使用监听器模式、观察者模式等等,另外spring框架本身提供的事件订阅机制,技术人员也可以使用(这里就不再铺展开讲解了,感兴趣的读者可以参看本专题后续文章,也可以参考其他第三方资料)。示例代码如下:

    /**
     * 用户模块中定义的事件信息,注意用户模块只是定义事件,而事件的实现交由上层模块进行
     * @author yinwenjie
     */
    public interface UserEventListener<T> {
      /**
       * 当用户模块完成新的用户信息创建时,该事件会被触发
       */
      public void onUserCreated();
      /**
       * 当用户模块由于某些原因,需要知晓上层功能模块中,业务信息和指定用户(多用户)的绑定情况时,
       * 该事件会被触发 
       */
      public List<T> onUserBandingInfoRequest(String account);
    }
    

    进行依赖倒转的本质是将本模块和其他模块逻辑相关的所有实现,由下层模块迁移到上层模块。例如以上示例中,将原有用户模块中直接调用的岗位实现逻辑迁移到岗位模块内部。这样,上层业务模块的逻辑情况就对下层模块透明了。岗位功能模块可以根据自身的情况,对这些事件进行实现(订阅),示例代码如下:

    // 岗位模块逻辑实现中依赖了用户模块的接口
    public class PositionServiceImpl implements PositionService , UserEventListener<YourBusiness> {
      // 用户模块不需要知晓岗位模块的存在
      // 只需要岗位模块依赖用户模块
      private UserService userService;
      public void doSomething() {
      }
      @Override
      public void onUserCreated() {
        // ..... 
      }
      @Override
      public List<YourBusiness> onUserBandingInfoRequest(String account) {
        // 根据岗位模块中的具体逻辑进行该事件实现
        return null;
      }
    }
    

    3.2、为功能模块规划标准的调用边界

    所有上层模块对其的调用,只能通过边界进入该功能模块。为了适应各种调用场景,支持功能模块的单向依赖,并统一功能模块边界的数据描述,功能模块的调用边界至少应该包括:标准的调用接口,标准的调用接口只有处于功能模块的上级模块才能直接使用,换句换说一旦外部功能模块直接调用了本功能提供的调用接口,那么外部功能模块一定处于该功能模块的上级;标准的模型定义,标准的模型定义规范了外部功能模块向该模块传递信息的统一要求,也规范了该模块向外部功能模块返回的处理结果(这类模型一般包括枚举信息,包括VO模型或DTO模型,一般不使用Entity进行描述);标准的事件定义,为了保证该模块能向上层模块通知自身的数据变化和逻辑处理要求,功能模块必须定义进行事件定义。

    3.3、模块实现应于模块边界分离

    功能模块有了明确的功能边界后,就为功能模块建立了一堵墙将模块外部和模块内部进行隔离,并且在墙上开了一道门。门外不需要知道门内的具体逻辑实现,而门内可以有若干种具体实现。换句话说,门内的具体实现也应该和这堵墙进行分离,以便应用系统可以在构建时选择需要哪种实现,如下所示:
    在这里插入图片描述
    Spring Boot中提供的组件开发模式(注意是组件开发模式,而不是插件开发模式),可以帮助设计人员快速实现这种分离场景的要求(这里不再展开,有兴趣的读者可参看本专题后续文章)。并且任意第三方对于功能模块的调用都需要通过门进入,如下图所示:
    在这里插入图片描述

    3.4、数据耦合和参数耦合是最低的耦合形态,应该尽可能使用

    降低模块间的耦合,并不是全面去除模块间的耦合,后者的理解是不科学的也不现实。需求要求的功能,需要两个或者多个模块配合完成,这些模块间当然就一定会有耦合。脱耦的关键目标在于明确功能模块的边界,在于将“你”要处理的内容和“我”要处理的内容分割清楚,在于可以达到就算没有“你”或者换一个“你”那么“我”也可以完整处理“我”负责内容的目的。

    要达到这个目标,就需要考虑如何进行耦合,很明显直接进行接口调用或者处理逻辑调用是不行的,而使用关键数据进行模块关联(数据耦合)并利用局部参数传递数据(参数耦合),可以有效减少两个模块的耦合程度。

    用一句很好理解的话来解释:模块和另一个模块耦合时,只在本模块中记录另一个模块的关键数据而不是全部数据,尽可能少记录另一个模块在本模块中的冗余数据,这样做的目的是保证就算另一个模块的具体工作逻辑被替换掉,本模块也可以根据这些关联数据精确驱动另一个模块的处理过程;而数据的传递和驱动要求的传递,通过局部参数或者对象属性完成,这保证了另一模块的处理逻辑不会牵扯另一模块中的其他逻辑处理过程。

    3.5、文档支持

    为了便于研发团队内部进行模块开发级别的交流,也便于二次开发团队了解模块的具体作用、使用方式、注意事项,软件研发过程特别是产品级别的软件研发过程,研发团队必须使用文档进行功能模块层面的描述。

    注意,这里说的是功能模块级别的描述,而不是具体业务实现逻辑的描述过程。这两份文档的区别主要体现在对不同技术层级的描述。具体业务实现的描述可以撰写成功独立文档,更推荐直接使用规范化的代码注释进行描述;而功能模块级别的文档必须独立成文,并使用利于团队交流的知识库系统进行管理。

    功能模块级别的文档至少应该描述以下事实:该模块在整个应用系统中的位置、该模块下层(直接)依赖了哪些模块以及原因、该模块提供了暴露的调用接口和事件订阅方式、该模块的在应用系统级别的引入方式等等。

    注意,相当一部分技术人员不习惯于写文档,或者说不知道如何写文档。为了在研发团队的磨合期帮助这部分技术人员上手文档写作,研发团队可以出具一份切实可行的文档模块,将文档分为几个段落并明确每个段落的写作要点、要求和示例,帮助引导技术人员的写作思路。以下为某产品研发过程中使用的模块描述文档的模板范例:
    在这里插入图片描述

    4、实际功能模块拆分举例

    下面以一个实际例子进行举例,两者相关联的业务需求点是:创建一个新的订货信息时,需要验证订货者是否还有未完成的退货单,如果有则不允许进行订货单创建。另外退货单创建时必须有关联的订货单,且订货单的状态必须是“已完成”,退货单创建过程中必须将对应的订货单置为“失效退货”状态。

    从需求层面上看,这两个模块的功能就应该是耦合在一起的,但这里要明确的是,作为研发团队我们不可能要求客户修改需求,而业务需求间的耦合并不是技术层面的耦合,研发设计的目的就是将业务需求解耦,转变为方面维护的一个一个独立功能模块。

    在没有进行好的模块化功能设计前,本系统中的订货功能和退货功能确实也是强耦合的方式撸(码)在了一起,如下所示:

    // 订货模块有如下代码
    // 退货单服务
    @Autowired
    private ChargebackService chargebackService;
    
    @Transactional
    public void create(OrderInfo orderinfo) {
      // ......
      // 验证订货者是否有未完成的退货单
      String account = orderinfo.getAccount();
      Set<ChargebackInfo> chargebackInfos = this.chargebackService.findByAccountAndStatus(account , Status.Enable);
      Validate.isTrue(CollectionUtils.isEmpty(chargebackInfos) , "订货者还有未完成的退单,不允许新建订货单!");
      // ......
    }
    
    // =====================
    
    // 退货单模块有如下代码
    // 退货服务
    @Autowired
    private OrderInfoService orderInfoService;
    @Transactional
    public void create(ChargebackInfo chargebackInfo) {
      // ......
      // 验证退货单的订单关联信息
      String relationCode = chargebackInfo.getRelationCode();
      OrderInfo exsitOrderinfo = orderInfoService.findByCodeAndStatus(relationCode);
      Validate.notNull(exsitOrderinfo , "未发现指定的订单信息!!");
      Validate.isTrue(exsitOrderinfo.getStatus() != Status.DONE  , "指定订单还未完成处理,不能进行退货!");
      
      // ...... 继续做退货单的其他处理
      // 然后在退货单模块,直接调用订单模块的接口,修改订货单状态
      this.orderInfoService.updateStatus(relationCode , Status.DONE);
      // ......
    }
    

    以上的示例代码是开发人员在实际系统开发过程中,编写的再简单不过的业务代码了。从需求的角度看这段代码没有问题,可以这样理解以上业务代码:就是开发人员按照需求人员对需求的描述,直接翻译成代码“贴”到应用系统中。从编码规范来看以上代码也没有问题:使用统一的命名规范、格式规范,有统一的边界校验控制,甚至使用统一的工具和编写技巧尽可能减少代码规模(这里特别说明一下,一些开发人员喜欢在开发过程中编写许多通用工具,例如字符串处理工具、日期处理工具、数值计算工具,并设想开发团队中的其他开发人员会使用这些工具,形成所谓的规范。这种做法是不科学、有危害的,原因会在本专题的后续文章中进行说明)。

    但是,以上代码从系统设计的角度看就存在问题了:订货单模块和退货单模块出现了强依赖,两个模块被循环依赖在了一起。如果产品团队根据需求分析最终决定订货模块和退货模块就应该是一个模块,那么这样做当然也没有什么大问题(就是后续要拆分成更细粒度的模块,会耗费大量工作),因为按照本文所述循环依赖只能存在于功能模块以内,如果循环依赖出现在模块间那么就证明模块拆分失败。

    在这里插入图片描述
    但是在本示例中,退货模块和订货模块显然属于两个需要独立工作的模块,那么必须通过系统设计的方式,降低这两个模块的耦合性,至少需要将两个模块的依赖方式变成单向依赖,将两个模块的耦合度降低到只有数据耦合和参数耦合。

    在进行设计前,我们先来确定一下这两个模块更科学的依赖方向:显然订货模块放置到更下层,可以使系统的依赖关系更科学,因为按照业务需求订货模块还将被除了退货模块以外的多个模块所依赖。如下图所示:
    在这里插入图片描述
    最终我们确认的模块拆分方案是:订单模块不应该引入任何退货模块的接口,甚至订单模块就不应该知道有一个退货模块。那么如何去掉订单模块中关于相关退货逻辑的处理呢?如何反转订单所依赖的退货单接口?

    设计模式中多种行为模式可以解决这个问题,最简单的方式就是为订单模块设计规范的事件接口,然后由上层模块根据自身业务需求实现这些事件(监听器模式/观察者模式)。我们先为订单模块定义标准的事件:

    /**
     * 订单事件,这个事件的定义在订单模块中
     * @author yinwenjie
     */
    public interface OrderEventListener {
      /**
       * 当订单被创建时(但本地事务还没有提交前),该事件被触发
       * @param orderInfo 本次新建的订单信息,通过参数方式进行传入
       */
      public void onCreated(OrderInfo orderInfo);
    }
    

    但是订单模块并不负责实现这些事件。接着,订单在自身的创建动作完成后,进行事件的触发,代码如下所示:

    // 订单模块的代码如下
    /**
     * 订单事件监听,之所以是集合,是因为可能有多个监听器的实现
     */
    @Autowired(required = false)
    private List<OrderEventListener> orderEventListeners;
    @Transactional
    public void create(OrderInfo orderinfo) {
      // ......
      // 在订单边界校验、自身处理过程完成后,触发事件
      if(!CollectionUtils.isEmpty(this.orderEventListeners)) {
        this.orderEventListeners.forEach(item -> item.onCreated(orderinfo));
      }
      // ......
    }
    

    这样的设计也基本能满足上文提到的进行模块设计的规范要求,特别是进行两个模块耦合的要求:首先采用监听器模式解决两个模块的循环依赖问题;然后退货单无论是直接调用订单的接口,还是实现订货模块的事件订阅都遵循订单模块向外暴露的标准边界(门),完全避免了和订单模块内的任何具体实现逻辑产生关系;最后退货模块和订单模块的耦合仅限于调用方法时传递的参数(事件中传递了订单对象信息),且退货单模块仅关联订单模块中的订单业务编号(在退货单模块中,该属性称为“第三方业务单据relationCode”),关联的目的是帮助订单模块的处理过程精确定位到相关的单据信息。

    这样一来,订单模块只需要将自身发生的变动的情况或者需要获取数据的事件发布出去,无需知道有哪些模块会订阅这些事件(订单模块除了数据层面、参数层面和各个上层模块有耦合以外,订单模块压根不知道有哪些上层模块会订阅事件,更谈不上知晓这些模块的作用)。
    在这里插入图片描述
    关键代码如下所示:

    // 此段代码是模块改造后,退货模块的代码示例
    // 该服务实现了订单模块的OrderEventListener监听接口
    public class ChargebackServiceImpl implements ChargebackService ,  OrderEventListener {
      
      @Override
      public void onCreated(OrderInfo orderInfo) {
        // 之前退货单放置在订单模块的代码放到了这里
        // 具体来说就是,验证订货者是否有未完成的退货单
        String account = orderinfo.getAccount();
        Set<ChargebackInfo> chargebackInfos = this.findByAccountAndStatus(account , Status.Enable);
        Validate.isTrue(CollectionUtils.isEmpty(chargebackInfos) , "订货者还有未完成的退单,不允许新建订货单!");
        // ...... 其它处理逻辑过程
      }
    }
    

    这是最简单的一种设计模式的应用方式。在这里如何进行事件的发布或者如何进行实现者行为的控制,完全取决于技术人员对需求的抽象能力,以及将抽象需求转换为设计思路的能力。再例如,当事件发生时系统中会有多个实现,但是只能按照条件选择一个最合适的实现进行调用,那么可以使用策略模式进行设计,如下所示:

    
    /**
     * 订单事件处理策略定义
     * @author yinwenjie
     */
    public interface OrderCreateEventStrategy {
      /**
       * 该方法将在订单创建事件发生后,首先被触发,
       * 系统将根据该方法的返回情况,确定是否使用该策略匹配本次订单创建后的处理逻辑
       * @param orderInfo 本次进行创建的订单
       * @return 如果返回true,则表示该处理策略逻辑将被正式执行;其他值,不执行该策略实现逻辑
       */
      public boolean isHandler(OrderInfo orderInfo);
      /**
       * 只有当本策略实现的isHandler方法返回true,该方法才会执行
       * @param orderInfo 本次进行创建的订单
       */
      public void onCeated(OrderInfo orderInfo);
    }
    

    接着本文再举一个例子:如果需要将事件的实现行为串起来执行,且需要按照业务逻辑对执行顺序进行管理,那么可以使用责任链模式进行设计(注意,责任链模式建议使用递归而非循序进行控制,最好准备责任链的上下文管理器[完全可以参考Servlet中filter的设计思路])。关键接口示例如下:

    /**
     * 订单模块为了事件处理,定义的责任链抽象类。
     * 事件策略逻辑过滤
     */
    public abstract class OrderEventFilter {
      /**
       * 该方法将在订单事件触发时,参与逻辑处理链
       * @param orderInfo 当前发生事件的订单
       * @param event 事件类型,包括DELETE,CREATE,UPDATE ......
       * @param orderEventHolder 订单事件管理器,是否进行后续处理或处理过程的上下文,由该对象控制
       */
      public abstract void handler(OrderInfo orderInfo , Event event , OrderEventHolder orderEventHolder);
    }
    

    有的读者会问,这些原则和示例是否只适用于单应用系统,如果应用系统是微服务架构又该怎么办呢?微服务架构同样需要遵从功能模块设计的原则,实际上本文的内容已经足可以帮助读者扩展出微服务系统下的模块构建方式。不过微服务系统由于涉及进程间通信,所以需要增加在另一些关键技术方案上的突破,例如如何保证多进程间的数据一致性(传统的基于数据库的分布式事务一定是不行的)、再例如怎么控制进程间的消息订阅和发布等等。这些坑在本专题的后续内容中将逐步填上。

    另外,本专题后续文章也会逐渐讨论与二次开发相关的实施方案,包括但不限于如何在功能模块内部完成开发(这种场景经常出现在以项目驱动的产品研发过程中),如何替换功能模块,如何将单一化应用系统改造为微服务系统等等。

    展开全文
  • 模块耦合与模块内聚

    千次阅读 2019-06-05 21:59:13
    模块的独立程度可以由两个定性标准来度量,这两个标准分别称为耦合和内聚。耦合衡量不同模块彼此间互相依赖的紧密程度;内聚衡量一个模块内部各个元素彼此结合的紧密程度。 1.耦合 耦合是对各个模块之间互连程度的...

    模块的独立程度可以由两个定性标准来度量,这两个标准分别称为耦合和内聚。耦合衡量不同模块彼此间互相依赖的紧密程度;内聚衡量一个模块内部各个元素彼此结合的紧密程度。

    1.耦合

    耦合是对各个模块之间互连程度的度量。耦合的强弱取决于接口的复杂性,即与信息传递的方式、接口参数的个数、接口参数的数据类型相关。不同模块之间互相依赖得越紧密则耦合程度越高。为了提高模块的独立性,应尽量降低模块之间的耦合程度。

    以下耦合程度由低到高:

    (1)无直接耦合:调用模块和被调用模块之间不存在直接的数据联系。
    (2)数据耦合:调用模块和被调用模块之间存在简单变量这样的数据传递。
    (3)标记(特征)耦合:调用模块和被调用模块之间存在数组、结构、对象等复杂数据结构的数据传递。
    (4)控制耦合:模块之间的联系不是数据信息,而是控制信息。
    (5)外部耦合:系统允许多个模块同时访问同一个全局变量。
    (6)公共耦合:允许多个模块同时访问一个全局性的数据结构。
    (7)内容耦合:允许一个模块直接调用另一个模块中的数据。

    注意:在软件设计时,开发人员应该尽量使用数据耦合,较少使用控制耦合,限制公共耦合的使用范围,同时坚决避免使用内容耦合。

    2.内聚

    模块的内聚是指模块内部各个元素之间 彼此结合的紧密程度。内聚和耦合往往密切相关,模块的高内聚通常意味着低耦合。在软件设计时,应该尽量提高模块的内聚程度,使模块内部的各个组成成分都相互关联,使其为了完成一个特定的功能而结合在一起。

    以下内聚程度由低到高:

    (1)偶然内聚:模块内各元素之间无实质性的联系,而只是偶然的组合在一起。
    (2)逻辑内聚:模块内部各组成成分的处理动作在逻辑上相似,但是功能却彼此不同。
    (3)时间内聚:指将若干在同一时间段内进行的却彼此不相关的工作集中在一个模块中。
    (4)过程内聚:模块内部各个成分按照确定的顺序进行并无相关联系的工作。
    (5)通信内聚:模块内部各个成分的输入数据和输出数据都相同。
    (6)顺序内聚:模块内的各个组成部分顺序执行,前一个成分的输出就是后一个成分的输入。
    (7)功能内聚:模块内的各个组成部分都为完成同一个功能而存在,强调完成并且只完成单一的功能。

    注意:软件系统中,要避免使用低内聚的模块,多使用高内聚尤其是功能内聚的模块。

    展开全文
  • 计算恒频发电控制参数,并以此为条件,对电场电压进行差动输入,实现电压恒频控制系统的软件运行环境搭建,联合相关硬件执行设备,完成基于电场耦合的分布式发电电压恒频控制系统设计。对比实验结果表明,与传统...
  • 模块设计采用了设计模式中的适配器模式和单件模式,解决了系统上层软件对采集控制设备函数耦合度高等问题。在系统的开发使用过程中,模块能够根据系统需求的变化更换采集控制设备而不更改上层软件,并保证设备稳定...
  • 为此设计了一种新的多物理场耦合下带电工具绝缘遮蔽性能检测系统系统硬件设计了3个模块,分别为控制器模块、驱动器模块、检测器模块,控制器模块内部选取FX1S-10MR-001型可编程控制器,驱动器模块包含电机驱动器、...
  • 模拟和实验均表明:该系统可将16支双管耦合进芯径105 μm、NA0.15的光纤,在注入电流为15 A,可获得稳定输出功率154 W,亮度达25 MW/(cm2·sr), 对应电光效率为42%。该模块工程化后可广泛应用在光纤激光器抽运...
  • 模块间的耦合

    千次阅读 热门讨论 2016-11-08 21:30:20
    模块间的耦合 耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。 耦合的强弱取决于 1.模块间接口的复杂 ...软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。 划分模块的一个准则

    模块间的耦合

    耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。

    耦合的强弱取决于

    1.模块间接口的复杂

    2.调用模块的方式

    3.通过界面传送数据的多少。

    模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。

    模块间联系越多,其耦合性越强,同时表明其独立性越差。

    软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。

    划分模块的一个准则就是高内聚低耦合。

     

    一般模块之间可能的连接方式有七种,构成耦合性的七种类型。它们之间的关系为(独立性由强到弱)

    计算机生成了可选文字:模 块 间 的 耦 合 低 非 直 接 耦 合 数 据 耦 合 标 记 耦 合 控 制 耦 合 外 部 耦 合 公 共 耦 合 内 容 耦 合 8 强 模 块 独 立 性

     

    非直接耦合(Nondirect Coupling)

    如果两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的,这就是非直接耦合。这种耦合的模块独立性最强。

    数据耦合(Data Coupling)

    如果一个模块访问另一个模块时,彼此之间是通过数据参数(不是控制参数、公共数据结构或外部变量)来交换输入、输出信息的,则称这种耦合为数据耦合。

    由于限制了只通过参数表传递数据,按数据耦合开发的程序界面简单、安全可靠。因此,数据耦合是松散的耦合,模块之间的独立性比较强。

    在软件程序结构中至少必须有这类耦合,多使用数据耦合

    印记耦合(Stamp Coupling)

    如果一组模块通过参数表传递记录信息,就是标记耦合

    事实上,这组模块共享了这个记录,它是某一数据结构的子结构,而不是简单变量。这要求这些模块都必须清楚该记录的结构,并按结构要求对此记录进行操作。

    在设计中应尽量避免这种耦合,它使在数据结构上的操作复杂化了。

    控制耦合(Control Coupling)

    如果一个模块通过传送开关、标志、名字等控制信息,明显地控制选择另一模块的功能,就是控制耦合。

    这种耦合的实质是在单一接口上选择多功能模块中的某项功能。因此,对所控制模块的任何修改,都会影响控制模块。

    另外,控制耦合也意味着控制模块必须知道所控制模块内部的一些逻辑关系,这些都会降低模块的独立性

    外部耦合(External Coupling)

    一组模块都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数表传递该全局变量的信息,则称之为外部耦合。

    公共耦合(Common Coupling)

    若一组模块都访问同一个公共数据环境,则它们之间的耦合就称为公共耦合。

    公共的数据环境可以是全局数据结构、共享的通信区、内存的公共覆盖区等

     这种耦合会引起下列问题:

    1.所有公共耦合模块都与某一个公共数据环境内部各项的物理安排有关,若修改某个数据的大小,将会影响到所有的模块。

    2.无法控制各个模块对公共数据的存取,严重影响软件模块的可靠性和适应性。

    3.公共数据名的使用,明显降低了程序的可读性。

     

    公共耦合的复杂程度随耦合模块的个数增加而显着增加。若只是两个模块之间有公共数据环境,则公共耦合有两种情况。

    若一个模块只是往公共数据环境里传送数据,而另一个模块只是从公共数据环境中取数据,则这种公共耦合叫做松散公共耦合

    若两个模块都从公共数据环境中取数据,又都向公共数据环境里送数据,则这种公共耦合叫做紧密公共耦合

    只有在模块之间共享的数据很多,且通过参数表传递不方便时,才使用公共耦合。否则,还是使用模块独立性比较高的数据耦合好些。

    内容耦合(Content Coupling)

    如果发生下列情形,两个模块之间就发生了内容耦合。

    一个模块直接访问另一个模块的内部数据;

    一个模块不通过正常入口转到另一模块内部;

    两个模块有一部分程序代码重叠(只可能出现在汇编语言中)

    一个模块有多个入口

    这种耦合是模块独立性最弱的耦合。

     

     

    总之尽量使用数据耦合,少量使用控制耦合,限制使用公共耦合,完全不用内容耦合。


    展开全文
  • 微想睿思之模块耦合

    千次阅读 2006-02-21 20:42:00
    系统设计中,模块高内聚低耦合是一项基本原则。也是设计模式,还有诸多流行框架所追求的根本。这一点无可厚非,我是绝对认同的。可是低耦合也应该有一个度,如果超过了这个度,看起来设计会很漂亮,但是具体实施的...

    系统设计中,模块高内聚低耦合是一项基本原则。也是设计模式,还有诸多流行框架所追求的根本。这一点无可厚非,我是绝对认同的。

    可是低耦合也应该有一个度,如果超过了这个度,看起来设计会很漂亮,但是具体实施的时候却发现难度很大,或者说单纯是为了设计而设计,甚至变成了为了模式而模式,为了框架而框架,就有点本末倒置了。这也就是所谓的过度设计吧。

    比如说利用XML解耦合。现在很多的流行框架,比如SPRING,HIBERNATE,STRUTS,EJB,都是利用它解耦合,将代码中的变化移到了XML中,好处当然是代码变动会很小,整个系统会很灵活,可是带来的负作用呢?XML文件本身会变得比较难写。并且庞大且不容易维护。而且在Debug的时候会发现很不直观。因为XML的错误往往是在运行时候才能体现。这就是一个平衡点的问题。假如你能够接受这样的设计方式,并且在实施的过程中感到难度不大,那么你就使用,反之可以尝试传统的编程方式。

    我们现在的这个项目就是借鉴了SOA的概念。利用XML作为UI和Service间数据交换的载体。因此传统的函数调用,都变成了XML数据交换。这样的好处当然是灵活,我对Service只用发一个HTTP请求,如果Service有实现,那么就还给我相应的XML。看起来很美,可是在实现的过程中,UI要求的数据结构千奇百怪,这样的Request大约有200多个,每个都要实现相应的XML,传统的一个函数调用变成了发请求,组合XML,解析XML,得出有用数据。实现难度实在是不小。对于Service的粒度划分,我们已经做到了尽可能合适。

    我想我们这样的设计,优点还是很明显的,实现的成本换来的是系统的灵活。这是一个度的问题,设计的真蒂我想就是在完成系统功能需求的基础上,尽量达到实现与设计的平衡。单纯的追求任何一方面都是不切实际的做法。

    展开全文
  • 在开发大型系统中,遵循这样一个原则:模块之间低耦合模块内高内聚。比如系统模块有界面模块和算法模块两种,一般是界面模块调用算法模块,这样的话界面模块依赖于算法模块。现在我要实现这样界面和算法分离,即...
  • 在最近的一次大数据技术讨论会上,本行业一家公司的技术高管谈到松耦合架构和紧耦合架构的性能表现的话题...写此博文,也希望给做系统设计的兄弟们,尤其是做高并发、复杂数据计算的同行提供一点参考。  先说紧耦合
  • 模块之间的耦合问题

    万次阅读 2015-12-17 21:30:00
    简介 一般模块之间可能的连接方式有七种,构成耦合性的七种类型。它们之间的关系为(独立性由强到弱) ...非直接耦合(Nondirect Coupling) ...如果两个模块之间没有直接关系,...如果一个模块访问另一个模块时,彼此之
  • 改进了CFD/CSD耦合系统,包括基于边界元方法设计了CFD和CSD耦合界面的数据转换方法,该方法可在同一映射矩阵处理结构响应和非定常气动载荷转换,保证了耦合边界上的能量守恒;改进了一种松耦合方法流程,在保持模块...
  • 写概要设计的时候设计类或者模块自然会考虑到“高内聚,低耦合” 什么是高内聚、低耦合? 【高内聚、低耦合】 内聚:每个模块尽可能独立完成自己的功能,不依赖于模块外部的代码。 耦合模块模块之间接口的...
  • 模块耦合和内聚

    千次阅读 2014-03-01 21:57:13
    模块耦合和内聚 分类: 软考2013-10-14 22:15 262人阅读 评论(5) 收藏 举报 目录(?)[+] 概述  模块的划分是软件结构化方法中提出来的想法,结构化方法的思想是,一个大问题分解成多...
  • 指软件系统结构中各模块间相互联系紧密程度的一种度量。模块之间联系越紧密,其耦合性就越强,模块的独立性则越差,模块耦合的高低取决于模块间接口的复杂性,调用的方式以及传递的信息。  形象的说,就是要将...
  • 介绍了一种智能照明系统设计,它利用STC89C52芯片作为控制核心, 对Light Emitting Diode(LED)照明系统和光纤照明系统实现耦合控制,取得了比单一LED照明系统更为节能和比单一光纤照明系统更稳定的效果。...
  • 软件设计-模块设计

    千次阅读 2019-04-25 18:22:58
    模块设计1.1 耦合模块之间的联系紧密程度1.2 内聚:模块内部各元素联系的紧密程度1.3 其他1.4 模块设计注意事项 1. 模块设计 模块设计基本原则:信息隐蔽,模块独立 1.1 耦合模块之间的联系紧密程度 1. 非直接...
  • 高内聚,低耦合 高内聚:一个C文件里面的函数...低耦合:一个完整的系统模块模块之间,尽可能的使其独立存在。也就是说,让每一个模块尽可能的独立完成某个特定的子功能。模块模块之间的接口应该尽量少而简单。
  • 耦合系统架构浅析

    千次阅读 2018-04-12 10:41:41
    阅读完本文后,可以从不同角度审视松耦合架构,通过统筹考虑产品不同阶段的架构设计,来提升系统架构质量。 作者介绍 刘光瑞,现任窝客研发总监,负责窝客产品研发管理及总体架构设计。拥有十几年企业级大型业务系统...
  • 我们的软交换系统设计的时候,参考了IMS中业务,控制,承载相分离的思想,及软交换只负责处理呼叫控制,对外提供呼叫控制的接口,通过应用服务器供业务程序调用,来完成特定的业务。对于交换机的增值业务,我们是...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 126,338
精华内容 50,535
关键字:

系统设计时模块耦合