架构整洁之道-第四部分-组件构建原则

211 阅读9分钟

组件

定义

组件要由一组关联的类和模块组成,一个组件要有明确的解决问题的方向

组件要保持可被独立部署和单独开发。

思考

组件是什么?

组件可以是一个java包,也可以是一个java应用也可以是一个java二方包,因此组件并没有明确的表明是什么,组件的核心是一组相关的类或者模块。

为什么要抽象组件?

前面定义中说【组件要有明确的解决问题的方向】,因此组件的存在就是解决软件架构中的某些问题,通过组件可以让软件架构能容易理解、上手。

组件的独立性

组件定义的最后明确组件需要保持可独立部署和单独开发,这个点其实就是第一个点的补充,一组相关的类或者模块让开发者更方便的开发和部署。

总结

对于组件的使用可以设置组件化的插件式架构。

组件聚合

复用、发布等同原则

定义

软件复用的最小力度应等于其发布的最小粒度

核心要点

构建组件的原则要由一类关联的类和模块构成

实际使用场景

2024-12-09 21.20.36.png 以目前我负责的调度平台为例,整个整个平台的架构组件分为四个:网关、调度中心、调度适配、调度数据。那么为什么这样分呢?

  • 网关组件将非业务、路由模块作为一个组件;
  • 调度中心将核心链路和底层协同组件schduleX作为一个应用,该应用需要有自己的数据库和应用;
  • 调度适配组件主要将业务类的代码作为一个实现
  • 调度数据基础组件则负责将所有的数据库操作进行收口,抽象各个数据模型通用的CRUD操作

通过这样的组件隔离和组件依赖关系,每个组件独立部署开发,很好的实现了复用、发布等同。

共同闭包原则

定义

我们应该将那些会同时修改,并且为相同目的而修改的类放到同一个组件中,而将不会同时修改,并且不会为了相同目的而修改的那些类放到不同的组件中。

核心要点

共同闭包的作用要求将所有可能会被修改的类集中在一处

实际使用场景

违反共同闭包的场景

2024-12-09 21.36.42.png 上图中两个应用因为初期有相同的部分代码,这部分代码包含数据库操作和数据的路由引擎部分,因此初期将两个应用相同的代码抽象为一个二方包,两个应用依赖同一个二方包。

设计之处并没有想到两个应用的发展完全不同,对于数据库的CRUD操作并不像初期那么稳定总是要改动一些数据操作,因此一个稳定的二方包就变成了一个要跟着应用的发布节奏每周两次打包,二方包成为了两个应用的累赘。

通过以上的描述其实也可以理解二方包应该保持稳定,因此这样的设计完全违反了共同闭包原则。

符合共同闭包的场景

2024-12-09 21.38.06.png 上图中的设计将CRUD抽象到每个应用中随着应用一起发展,针对路由引擎部分因为基本不做改动所以依然提供二方包。

这样将改动频率较大的代码整合到应用中符合复用、发布等同原则,而路由引擎的二方包则独立一个二方包(组件),符合【为相同目的而修改的类放到同一个组件中】

思考

通过以上的例子可以思考一个点:复用性和可维护性哪个更重要?

我的回答是【可维护性】。

开发者有时并没有合理的考虑复用的场景,纯粹为了复用而复用,觉得代码相同就应该抽象为一个方法或者一个组件,但是两个场景的发展轨迹出现差异时,那么抽象复用方法或者组件则会导致复杂度上升并且难以维护,极容易出现各种if条件判断。

#可维护性的重要性大于可复用性。

共同复用原则

定义

一个组件应该依赖自己需要的东西,避免依赖不需要的东西

核心要点

不是紧密相连的类不应该放在一个组件中(跟单一职责类似)

跟单一职责类似那么区别是什么呢?

  • 单一职责体现的是不要依赖带有不需要的函数的类
  • 共同复用体现的是不要依赖带有不需要的类的组件

实际使用场景

违反共同复用原则

2024-12-09 22.11.11.png 上图中订单回传时需要依赖一个回传基础配置,而这个配置开发之初由于二方包使用相同的数据库因此直接将该功能放在了二方包中,这样就导致订单接收也依赖了该功能的相关类。

这样的方式就违反了共同复用原则

符合共同复用原则

2024-12-09 22.11.34.png 上图中奖回传基础配置抽取到订单回传应用中,订单接收并不需要该功能因此不需要依赖相关类。

思考

软件开发中如何考虑复用?

我的理解是针对已经明确发展轨迹不同的组件则可以将相同的类抽象为相同的组件。

对于发展轨迹目前不明确的组件则要做到隔离,避免复用,还是前面提到的【可维护性大于复用性】

组件耦合

无依赖环原则

定义

组件依赖关系图中不应该出现环

核心要点

将项目划分为可单独发布的组件,组件交由单人或者一组程序员来独立完成

实际使用场景

2024-12-09 22.21.30.png 上图中正常的以来关系应该是自上而下,这样的以来关系可以保证自上而下发布的顺序可以实现发布gateway不会影响center、adater、parcel

但是如果出现adapter依赖gateway则就出现了依赖环,那么发布gateway就会影响adapter。(这个不是绝对的,如果想要依赖gateway可以使用依赖翻转的方式)

自上而下的设计

定义

组件结构图需要随着软件系统的变化而变化和扩展

核心要点

频繁变更的组件不要影响到其他稳定的组件

实际使用场景

gateway、center、adapter的依赖关系为gateway--->center---->adapter,因此应用发布时需要先发布gateway、center、adapter,这样避免从低向上发布对其他依赖的应用造成影响。

稳定依赖原则

定义

依赖关系必须指向更稳定的方向

实际使用场景

2024-12-09 22.11.34.png 上图中依赖关系指向更稳定的方向都依赖稳定的二方包

思考

稳定性判定

带有许多入向依赖关系的组件是非常稳定的,即哪个组件被指向的数量越多则稳定性越高

稳定抽象原则

定义

一个组件的抽象化程度应该与其稳定性保持一致。

如何理解这句话?

  • 稳定性越高,抽象化越高,稳定性越低,抽象化越低。

核心要点

一个组件要想成为稳定组件那么它就应该由接口和抽象类组成,以便将来做扩展

实际使用场景

2024-12-09 22.47.57.png 上图中实现下发和回传两个链路,那么两个了链路可能存在相同的逻辑,因此通过接口和抽象类对逻辑做抽象。

  • BaseHandler:基础接口,定义核心执行方法和类型
  • AbstractHandler:抽象类,抽象核心执行的方法,比如解析参数、执行、后置执行、组装响应参数
  • AbstracSendHandler/AbstractCallbackHandler:抽象核心链路下发和回传,每个核心链路实现execute方法,基于核心执行方法设置execute的模版
  • xxxHandler:具体的业务类
/**  
 * 最高级抽象,抽象处理器定义最高级别的抽象方法  
 * @author CLL02  
 * @param <A>  
 */  
public interface BaseHandler<A> {  
  
    void execute(A a);  
  
    String handlerType();
}

/**  
 * 处理抽象基类  
 * 1. 对父类的抽象方法做默认实现  
 * 2. 定义业务型的抽象方法  
 * @author CLL02  
 */public abstract class AbstractHandler<T extends BasePacket, K extends BaseContext> implements  
        BaseHandler<K> {  
  
    protected void parseParam(BaseContext context) {  
  
    }  
    protected void preHandle(K context) {  
    }  
    protected void handler(K context) {  
  
    }  
  
    protected void afterHandle(final K context) {  
    }   
  
    protected T buildResponse(BaseContext context) {  
        return null;  
    }  
  
}



/**  
 * @author CLL02  
 * 定义核心业务流程的抽象类:回传核心流程的抽象类  
 * 1. 通过模板模式抽象业务流程  
 * 2. 定义抽象方法用于子类做具体实现  
 */  
public abstract class  AbstractCallbackHandler<T extends BaseCallbackPacket>  
    extends AbstractHandler<T, BaseContext> {  
  
  
    @Override  
    public void execute(BaseContext context) {  
        //准备参数  
        parseParam(context);  
        //前置操作  
        preHandle(context);  
        //执行逻辑
        handler(context);  
        // 后置操作  
        afterHandle(context);  
        buildResponse(context);  
    }
}
/**  
 * @author CLL02  
 * 定义核心业务流程的抽象类:下发核心流程的抽象类  
 * 1. 通过模板模式抽象业务流程  
 * 2. 定义抽象方法用于子类做具体实现  
 */  
public abstract class  AbstractCallbackHandler<T extends BaseCallbackPacket>  
    extends AbstractHandler<T, BaseContext> {  
  
  
    @Override  
    public void execute(BaseContext context) {  
        //准备参数  
        parseParam(context);  
        //前置操作  
        preHandle(context);  
        //执行逻辑
        handler(context);  
        // 后置操作  
        afterHandle(context);  
        buildResponse(context);  
    }
}



/**  
 * @author CLL02  
 * 入库回传流程业务处理  
 */  
@Service  
public class xxxCallbackHandler  
        extends AbstractCallbackHandler<xxxPacket> {
	//重写parseParam
	//重写preHandle
	//重写handler
	//重写afterHandle
	//重写buildResponse
}


以上设计的链路中,我们将通用并且不经常被修改的逻辑抽象为接口或者抽象类,接口和抽象类是对业务的抽象,抽象的程度越高对业务的改动越小,因此抽象的越高稳定性越高。基本上抽象类不会做改动,具体业务各自重写抽象方法即可。