工作10余年我对应用结构和命名的理解

232 阅读23分钟

--清晰的代码结构和命名让人舒舒服服

序言

我们开发代码的时候,类的命名和分层无疑是广大后端小伙伴们的两大痛点,尤其是纠结于一个类的命名的时候,英文不好的小伙伴,直接翻译,然后按照自己的理解,可能会写下另别人无法理解的名称,给大家造成很大的苦恼,其实应用结构和命名是有讨论的,本文就总结一下基于作者10余年的一线代码开发经验,提供一种基于领域模型的轻量级应用分层结构设计和命名的浅薄见解。

一 分层架构

image.png

接入层(前端请求层)

通过调用业务层服务,处理前端的请求。接入层其实还可以根据业务场景再分为API层和适配器层,适配器分为请求适配器和外部请求适配器,一个外部请求通过适配器转换为访问我们自己的应用,一个是把我们的数据通过适配器转换为访问外部应用。其实都是接入层的更加详细的分类。

业务逻辑层

提供封装好的能力,并通过对能力进行组装、编排,进行业务逻辑处理。

基础资源层

对底层数据源进行增删改查操作。

其实在业务层和基础资源层中间还有一个领域层,它是负责对领域的核心抽象,提供原子服务,由业务逻辑层进行业务组装,领域层的知识大家自行网上搜索,对于基础和高级开发者领域可能比较难理解,不过无关紧要,我们的三层架构也可以很好的满足需求。简单即合理。

扩展知识:其实不仅仅还有分层架构,还有DDD架构,整洁架构,六边形架构。大家自行网上去搜索,本文的核心不是讨论此内容,不做过多阐述。

二 分层明细

接入层

子包描述
controller 对接前端的控制器
model 前端请求相关的对象
request前端/上游传入的请求对象
response返回给前端/上游的对象
convert controller请求转化为service请求的转化类、dto转化为vo的转化类

DTO和VO不清晰概念的,可以参考附录:PO,VO,DAO,BO,DTO,POJO介绍。

业务逻辑层

子包描述
service 查询服务和领域服务
query查询服务
order领域服务。示例是订单领域service
ability 领域能力
order举例:订单域领能力
manager 对应底层数据模型的通用逻辑处理器
remote 外部服务
messageproducer消息生产者
consumer消息消费者
diamond 动态配置
tair/redis 缓存服务
config 业务层配置项,如:bean配置
common 内部公共类
constansts仅内部使用的常量
convertdto和vo的转化器、service请求转化为manager请求的转化器
enums仅内部使用的枚举
model.dto用于业务处理的实体类载体
model.requestservice和ability的请求类
utils工具包

如上只是部分举例,其实service下还分为impl实现service,impl还有async异步,以及各个小的领域包吗,例如order/user/activity等等。这里不做过多的介绍。

基础资源层

子包描述
mq mq底层操作能力
redis 缓存底层操作能力
dalmapper 
 entity 

基础资源层包含的比较多,我们直接给分开,mq,redis,dal,外部请求都可以归属于基础资源,我们把它们归类在一起,作为我们的支撑,提供操作最底层的原子支撑。当然还有xxljob定时任务等等,分为单独的包管理即可。

start启动类

test 测试类

三 调用关系

image.png

注意点****

服务和服务直接可以互相调用;

服务可以调用多个域的域能力;

领域能力manager是封装好的最小颗粒度的能力,不可互相调用;

业务层搭配基础资源层调用关系是一个示例。也可能是业务A直接到基础资源层

四 各层规范

接入层

定义统一的异常处理切面:处理业务异常和其他运行时异常;

业务逻辑层

内部服务不做异常处理和返回result封装类,异常都抛给接入层;

能力唯一对应一个领域,且是封装好的最小颗粒度的能力。

外部服务要在remote中做好异常处理和封装;

业务层中的common类为仅在应用内部使用的公共类;

基础资源层

mapper要按不同类型的领域分开存放,如user和order或者activity。

五 各层类和方法命名

上述把项目的整体骨架进行了划分,日常编码中,代码的命名也是个大的学问。能快速的看懂开源软件的代码结构和意图,也是一项必备的能力。那它们有什么我们默认的习惯呢?

为了让大家更好的理解命名的套路,我借鉴了最流行的Java接开源软件(spring系列,netty,libgdx,guava,logback等等),总结了10类常见的类命名。大多数是以后缀形式存在的,也有不少可以组合使用,用来表达多重的意义。

管理类命名

代码时,统一管理资源至关重要。一个清晰的启动流程有助于有效组织代码,确保程序的稳定运行。这包括资源的注册、调度,以及公共集合资源的管理。合理的资源管理和启动流程可以简化代码结构,提高代码的可读性和可维护性。

Bootstrap,Starter

一般作为程序启动器使用,或者作为启动器的基类。通俗来说,可以认为是main函数的入口。

image.png

Processor

当你面对一组用于处理某一类功能的代码片段,且这些代码片段按照一定顺序执行时,你可以考虑使用“处理器”(Processor)这一术语来命名它们。这样做不仅可以清晰地表示这组代码的功能和用途,还能为代码库增添一种结构化和模块化的风格。

例如,如果你有一组代码负责处理用户登录的逻辑,你可以将这些代码片段集合起来,命名为“LoginProcessor”或类似的名称。这个名字明确指出了这组代码的功能——处理用户登录的过程,同时也暗示了这些代码是按照一定顺序或逻辑来执行的。

使用“处理器”这一术语来命名代码片段集合,可以使你的代码库更加易于理解和维护。当其他开发者看到这样的命名时,他们可以迅速理解这组代码的作用和用途,从而更容易地参与到项目的开发中来。

image.png

Manager

对于有生命状态的对象进行管理时,通常会作为一个资源管理的入口,这样的组件可以被称为“管理器”(Manager)或“控制器”(Controller)。这些组件负责追踪和控制对象的生命周期,包括创建、更新、删除等操作,以及可能的状态转换。

使用“管理器”或“控制器”作为这样的组件的命名,可以清晰地表达其职责和功能,即管理具有生命状态的对象。这样的命名约定有助于提高代码的可读性和可维护性,使得开发者能够更快地理解和使用这些组件。

image.png

Holder

表示持有某个或者某类对象的引用,并可以对其进行统一管理。多见于不好回收的内存统一处理,或者一些全局集合容器的缓存。

image.png

Factory

工厂模式(Factory Pattern)是一种在编程中广泛使用的创建型设计模式。它提供了一种封装机制来将对象的创建与使用分离,使得代码更加灵活和可维护。在Spring框架中,工厂模式的应用尤为普遍,因为Spring的核心功能之一就是管理对象的生命周期,并通过工厂来创建和管理Bean。

image.png

Provider

简化来说,Provider 可以被视为一种更高级别的设计模式或组件,它结合了策略模式(Strategy Pattern)和方法工厂模式(Factory Method Pattern)的特性。通过Provider,我们可以灵活地切换不同的实现策略,并且隐藏了具体对象的创建细节。

在Provider模式中,通常会有一个接口或抽象类作为Provider的基础定义,这个接口或抽象类会声明一些方法,这些方法对应于不同的策略或功能。每个具体的实现类(即策略)都会实现这个接口或继承这个抽象类,并提供相应的功能实现。

image.png

Registrar

注册并管理一系列资源。

image.png

Engine

引擎(Engine)通常指的是一个软件或系统的核心模块,它负责处理一类核心或复杂的功能。由于“引擎”这个词在技术和专业领域中具有高级和专业的含义,因此并不是所有的类都有资格使用它作为命名。通常,只有那些承担了系统核心功能、具有复杂性和高性能要求的组件才会被命名为“引擎”。这样的命名方式能够突显其重要性、专业性和技术价值。

image.png

Service

某个服务。太简单,不忍举例。范围太广,不要滥用哦。

image.png

Task

某个任务。通常是个runnable

image.png

传播类命名

为了完成一些统计类或者全局类的功能,有些参数需要一传到底。传播类的对象就可以通过统一封装的方式进行传递,并在合适的地方进行拷贝或者更新。

Context

如果你的程序执行,有一些变量,需要从函数执行的入口开始,一直传到大量子函数执行完毕之后。这些变量或者集合,如果以参数的形式传递,将会让代码变得冗长无比。这个时候,你就可以把变量统一塞到Context里面,以单个对象的形式进行传递。

在Java中,由于ThreadLocal的存在,Context甚至可以不用在参数之间进行传递。

image.png

Propagator

传播,繁殖。用来将context中传递的值进行复制,添加,清除,重置,检索,恢复等动作。通常,它会提供一个叫做propagate的方法,实现真正的变量管理。

image.png

回调类命名****

使用多核可以增加程序运行的效率,不可避免的引入异步化。我们需要有一定的手段,获取异步任务执行的结果,对任务执行过程中的关键点进行检查。回调类API可以通过监听、通知等形式,获取这些事件。

Handler,Callback,Trigger,Listener

callback通常是一个接口,用于响应某类消息,进行后续处理;Handler通常表示持有真正消息处理逻辑的对象,它是有状态的;tigger触发器代表某类事件的处理,属于Handler,通常不会出现在类的命名中;Listener的应用更加局限,通常在观察者模式中用来表示特定的含义。

image.png

Aware****

Aware就是感知的意思,一般以该单词结尾的类,都实现了Aware接口。拿spring来说,Aware 的目的是为了让bean获取spring容器的服务。具体回调方法由子类实现,比如ApplicationContextAware。它有点回调的意思。

image.png

监控类命名

现在的程序都比较复杂,运行状态监控已经成为居家必备之良品。监控数据的收集往往需要侵入到程序的边边角角,如何有效的与正常业务进行区分,是非常有必要的。

Metric

表示监控数据。不要用Monitor了,比较丑。

image.png

Estimator

估计,统计。用于计算某一类统计数值的计算器。

image.png

Accumulator

累加器的意思。用来缓存累加的中间计算结果,并提供读取通道。

image.png

Tracker

一般用于记录日志或者监控值,通常用于apm中。

image.png

内存管理类命名

如果你的应用用到了自定义的内存管理,那么下面这些名词是绕不开的。比如Netty,就实现了自己的内存管理机制。

Allocator

与存储相关,通常表示内存分配器或者管理器。如果你得程序需要申请有规律得大块内存,allocator是你得不二选择。

image.png

Chunk

表示一块内存。如果你想要对一类存储资源进行抽象,并统一管理,可以采用它。

image.png

Arena

英文是舞台、竞技场的意思。由于Linux把它用在内存管理上发扬光大,它普遍用于各种存储资源的申请、释放与管理。为不同规格的存储chunk提供舞台,好像也是非常形象的表示。

关键是,这个词很美,作为后缀让类名显得很漂亮。

image.png

Pool

表示池子。内存池,线程池,连接池,池池可用。

image.png

过滤检测类命名

程序收到的事件和信息是非常多的,有些是合法的,有些需要过滤扔掉。根据不同的使用范围和功能性差别,过滤操作也有多种形式。你会在框架类代码中发现大量这样的名词。

Pipeline,Chain

一般用在责任链模式中。Netty,Spring MVC,Tomcat等都有大量应用。通过将某个处理过程加入到责任链的某个位置中,就可以接收前面处理过程的结果,强制添加或者改变某些功能。就像Linux的管道操作一样,最终构造出想要的结果。

image.png

Filter

过滤器,用来筛选某些满足条件的数据集,或者在满足某些条件的时候执行一部分逻辑。如果和责任链连接起来,则通常能够实现多级的过滤。

image.png

Interceptor

拦截器,其实和Filter差不多。不过在Tomcat中,Interceptor可以拿到controller对象,但filter不行。拦截器是被包裹在过滤器中。

image.png

Evaluator

英文里是评估器的意思。可用于判断某些条件是否成立,一般内部方法evaluate会返回bool类型。比如你传递进去一个非常复杂的对象,或者字符串,进行正确与否的判断。

image.png

Detector

探测器。用来管理一系列探测性事件,并在发生的时候能够进行捕获和响应。比如Android的手势检测,温度检测等。

image.png

结构类命名

除了基本的数据结构,如数组、链表、队列、栈等,其他更高一层的常见抽象类,能够大量减少大家的交流,并能封装常见的变化。

 

Cache

这个没啥好说的,就是缓存。大块的缓存。常见的缓存算法有LRU、LFU、FIFO等。

Buffer

buffer是缓冲,不同于缓存,它一般用在数据写入阶段。

image.png

Composite

将相似的组件进行组合,并以相同的接口或者功能进行暴露,使用者不知道这到底是一个组合体还是其他个体。

Wrapper

用来包装某个对象,做一些额外的处理,以便增加或者去掉某些功能

image.png

Option, Param,Attribute

用来表示配置信息。说实话,它和Properties的区别并不大,但由于Option通常是一个类,所以功能可以扩展的更强大一些。它通常比Config的级别更小,关注的也是单个属性的值。Param一般是作为参数存在,对象生成的速度要快一些。

Aggregator

聚合器,可以做一些聚合计算。比如分库分表中的sum,max,min等聚合函数的汇集。

image.png

Iterator

迭代器。可以实现Java的迭代器接口,也可以有自己的迭代方式。在数据集很大的时候,需要进行深度遍历,迭代器可以说是必备的。使用迭代器还可以在迭代过程中安全的删除某些元素。

image.png

Batch

某些可以批量执行的请求或者对象。

image.png

Limiter

限流器,使用漏桶算法或者令牌桶来完成平滑的限流。

image.png

常见设计模式命名

设计模式是名词的重灾区,这里只列出最常使用的几个。

Strategy

将抽象部分与它的实现部分分离,使它们都可以独立地变化。策略模式。相同接口,不同实现类,同一方法结果不同,实现策略不同。比如一个配置文件,是放在xml里,还是放在json文件里,都可以使用不同的provider去命名。

image.png

Adapter

将一个类的接口转换为客户希望的另一个接口,Adapter模式使得原本由于接口不兼容而不能一起工作的那些类一起工作。

不过,相对于传统的适配器进行api转接,如果你的某个Handler里面方法特别的多,可以使用Adapter实现一些默认的方法进行0适配。那么其他类使用的时候,只需要继承Adapter,然后重写他想要重写的方法就可以了。这也是Adapter的常见用法。

image.png

Action,Command

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

用来表示一系列动作指令,用来实现命令模式,封装一系列动作或者功能。Action一般用在UI操作上,后端框架可以无差别的使用。

在DDD的概念中,CQRS的Command的C,既为Command。

image.png

event

表示一系列事件。一般的,在语义上,Action,Command等,来自于主动触发;Event来自于被动触发。

image.png

Builder

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

构建者模式的标准命名。比如StringBuilder。当然StringBuffer是个另类。这也说明了,规则是人定的,人也可以破坏。

image.png

Template

模板方法类的命名。定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

image.png

Proxy

代理模式。为其他对象提供一种代理以控制对这个对象的访问。

image.png

解析类命名

写代码要涉及到大量的字符串解析、日期解析、对象转换等。根据语义和使用场合的区别,它们也分为多种。

Converter,Resolver

转换和解析。一般用于不同对象之间的格式转换,把一类对象转换成另一类。注意它们语义上的区别,一般特别复杂的转换或者有加载过程的需求,可以使用Resolver。

image.png

Parser

用来表示非常复杂的解析器,比如解析DSL。

image.png

Customizer

用来表示对某个对象进行特别的配置。由于这些配置过程特别的复杂,值得单独提取出来进行自定义设置。

image.png

Formatter

格式化类。主要用于字符串、数字或者日期的格式化处理工作。

image.png

网络类命名

网络编程的同学,永远绕不过去的几个名词。

Packet

通常用于网络编程中的数据包。

image.png

Protocol

同样用户网络编程中,用来表示某个协议。

image.png

Encoder、Decoder、Codec

编码解码器

image.png

Request,Response****

一般用于网络请求的进和出。如果你用在非网络请求的方法上,会显得很怪异

CRUD命名

这个就有意思多了,统一的Controller,Service,Repository,没什么好说的。但你一旦用了DDD,那就得按照DDD那一套的命名来。

由于DDD不属于通用编程范畴,它的名词就不多做介绍了。

其他

Util,Helper

都表示工具类,Util一般是无状态的,Helper以便需要创建实例才能使用。但是一般没有使用Tool作为后缀的。

image.png

Mode,Type

看到mode这个后缀,就能猜到这个类大概率是枚举。它通常把常见的可能性都列到枚举类里面,其他地方就可以引用这个Mode。

image.png

Invoker,Invocation

invoker是一类接口,通常会以反射或者触发的方式,执行一些具体的业务逻辑。通过抽象出invoke方法,可以在invoke执行之前对入参进行记录或者处理;在invoke执行之后对结果和异常进行处理,是AOP中常见的操作方式。

image.png

Initializer

如果你的应用程序,需要经过大量的初始化操作才能启动,那就需要把它独立出来,专门处理初始化动作。

image.png

Future,Promise

它们都是用在多线程之间的,进行数据传递。

Future相当于一个占位符,代表一个操作将来的结果。一般通过get可以直接阻塞得到结果,或者让它异步执行然后通过callback回调结果。

但如果回调中嵌入了回调呢?如果层次很深,就是回调地狱。Java中的CompletableFuture其实就是Promise,用来解决回调地狱问题。Promise是为了让代码变得优美而存在的。

Selector

根据一系列条件,获得相应的同类资源。它比较像Factory,但只处理单项资源。

image.png

Reporter

用来汇报某些执行结果。

image.png

Constants

一般用于常量列表。

Accessor

封装了一系列get和set方法的类。像lombok就有Accessors注解,生成这些方法。但Accessor类一般是要通过计算来完成get和set,而不是直接操作变量。这适合比较复杂的对象存取服务。

image.png

Generator

生成器,一般用于生成代码,生成id等。

image.png 六 总结

本文就讲述这么多,从架构的骨架到细节的名称命名都讲述了一下,读万卷书,不如行万里路,大家试着再写代码的时候,强制按照此架构风格和代码命名习惯进行书写,后面你慢慢的就会体会到它的便捷性。只可意会不可言传。

 

附录****

PO,VO,DAO,BO,DTO,POJO介绍

阿里巴巴Java开发规范》关于领域模型的部分介绍如下

分层领域模型规约:

· DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。

· DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。

· BO(Business Object):业务对象,由 Service 层输出的封装业务逻辑的对象。

· AO(ApplicationObject):应用对象,在Web层与Service层之间抽象的复用对象模型, 极为贴近展示层,复用度不高。

· VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。

· Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。

领域模型命名规约:

· 数据对象:xxxDO,xxx即为数据表名

· 数据传输对象:xxxDTO,xxx为业务领域相关的名称。

· 展示对象:xxxVO,xxx一般为网页名称。

· POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。

一、PO :(persistant object ),持久对象

可以看成是与数据库中的表相映射的java对象。使用Hibernate来生成PO是不错的选择。

二、VO :(value object) ,值对象

通常用于业务层之间的数据传递,和PO一样也是仅仅包含数据而已。但应是抽象出的业务对象,可以和表对应,也可以不,这根据业务的需要。

PO只能用在数据层,VO用在商业逻辑层和表示层。各层操作属于该层自己的数据对象,这样就可以降低各层之间的耦合,便于以后系统的维护和扩展。

三、DAO :(Data Access Objects) ,数据访问对象接口

DAO是Data Access Object数据访问接口,数据访问:顾名思义就是与数据库打交道。夹在业务逻辑与数据库资源中间。

J2EE开发人员使用数据访问对象(DAO)设计模式把底层的数据访问逻辑和高层的商务逻辑分开.实现DAO模式能够更加专注于编写数据访问代码。

DAO模式是标准的J2EE设计模式之一.开发人员使用这个模式把底层的数据访问操作和上层的商务逻辑分开.一个典型的DAO实现有下列几个组件:

1. 一个DAO工厂类;

2. 一个DAO接口;

3. 一个实现DAO接口的具体类;

4. 数据传递对象(有些时候叫做值对象)。

具体的DAO类包含了从特定的数据源访问数据的逻辑。

四、BO :(Business Object),业务对象层

表示应用程序领域内“事物”的所有实体类。这些实体类驻留在服务器上,并利用服务类来协助完成它们的职责。

五、DTO Data Transfer Object数据传输对象

主要用于远程调用等需要大量传输对象的地方。比如我们一张表有100个字段,那么对应的PO就有100个属性。但是我们界面上只要显示10个字段,客户端用WEB service来获取数据,没有必要把整个PO对象传递到客户端,这时我们就可以用只有这10个属性的DTO来传递结果到客户端,这样也不会暴露服务端表结构.到达客户端以后,如果用这个对象来对应界面显示,那此时它的身份就转为VO

六、POJO :(Plain Old Java Objects),简单的Java对象

实际就是普通JavaBeans,使用POJO名称是为了避免和EJB混淆起来, 而且简称比较直接.其中有一些属性及其getter、setter方法的类,有时可以作为value object或dto(Data Transform Object)来使用。

当然,如果你有一个简单的运算属性也是可以的,但不允许有业务方法,也不能携带有connection之类的方法。