架构师技能1:Java工程规范、浅析领域模型VO、DTO、DO、PO、优秀命名

架构师技能1:Java工程规范、浅析领域模型VO、DTO、DO、PO、优秀命名把我们之前项目的相关规范做了个总结记录下来 仅供参考 望能有点帮助 每个人的代码风格迥异 通过统一的编码风格使得代码可读性大大提高 编程规约或者编码规范的的本质是提高了代码的可读性 最终目的是提高团队协作效率 降低工程维护成本 一 项目的应用分层 代码分层

大家好,我是讯享网,很高兴认识大家。

把我们之前项目的相关规范做了个总结记录下来,仅供参考,望能有点帮助。

每个人的代码风格迥异,通过统一的编码风格使得代码可读性大大提高。

编程规约或者编码规范的的本质是提高了代码的可读性,最终目的是提高团队协作效率,降低工程维护成本。

一、项目的应用分层:


代码分层,让不同层次的代码做不同的动作。层次清晰的代码,提高可读性,从代码结构就大概能了解到代码是如何分层,每层大概功能是什么。例如常用的Controller、Service、Mapper/Dao三层代码结构,其各层的代码逻辑范围。

默认上层依赖于下层,箭头关系表示可直接依赖,如:开放接口层可以依赖于Web 层,也可以直接依赖于 Service 层,依此类推:


讯享网

1、开放接口api层/controller: 可直接封装 Service 接口暴露成 RPC 接口; 通过 Web 封装成 http 接口; 网关控 制层等。

2、Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。

3、Service 层:相对具体的业务逻辑服务层。

4、Manager 层:通用业务处理层,它有如下特征

  • 1)封装下沉通用能力 对 Service 层通用能力的下沉,如缓存方案、 中间件通用处理;如mqMannager。
  • 2)封装第三方接口:对第三方平台封装的层,预处理返回结果及转化异常信息,如rpcMannager。
  • 3) 与 DAO 层交互,对 DAO 的业务通用能力的封装。

5、DAO 层:持久层,数据访问层,与底层 MySQL、 Oracle、 Hbase 进行数据交互,用来封装对数据库的访问(CRUD)

6、Model, 领域模型对象:   (第二节专门讨论) 

7、外部接口或第三方平台: 包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。

分层对于代码规范是比较重要,决定着以后的代码是否可复用,是否职责清晰,边界清晰。分层其实见仁见智,只要符合团队规范就可以。比如我们之前的团队工程规范:

api层/controller:轻业务逻辑,参数校验,异常兜底。通常这种接口可以轻易更换接口类型,所以业务逻辑必须要轻,甚至不做具体逻辑。该层的领域模型不允许传入DAO层。

Service:业务层,复用性较低,推荐每一个controller方法都得对应一个service,不要把业务编排放在controller中去做。允许操作DO数据领域模型Model。

在这一层设置事务:@Transactional(rollbackFor = Exception.class),防止事务混乱设置,导致一些不可控的事物异常。

Mannager:高可复用逻辑层。主要是做的是业务逻辑处理和数据组装。允许操作DO数据领域模型Model。在这一层是不允许设置事务,异常往外抛出,防止事务混乱设置,导致一些不可控的事物异常。

service层之间是否可以相互调用?

这个需要看项目规范,适合业务即可,软件最重要的还是是解决复杂度问题,如果service之间是通过接口依赖、IoC注入和自动事物机制,Service层可以互相调用。

二、领域模型


1、Model, 领域模型对象

  领域模型POJO 一般是 DO/DTO/BO/VO 的统称,即POJO 专指只有 setter / getter / toString 的简单类,一般无业务逻辑代码,包括 DO/DTO/VO/PO 等。

  • DO (Domain Object):主要用于定义与数据库对象应的属性(ORM对象关系映射),实体bean的DO映射成一张表,通过 DAO 层向上传输数据源对象。看项目的编码规约,有的习惯使用命名为XxxEntity, 有的习惯命名为XxxDo, 比如UserEntity或者UserDO           
  • DTO数据传输对象(Data Transfer Object):系统、服务之间交互传输用或者是传输类间数据, Service 或 Manager 向外传输的对象。  看项目的编码规约,比如我们习惯使用DTO来做rest 接口参数或者rpc参数。
  • VO View Object:  视图对象,用于展示层,  前台(APP\WAP\PC)展示用;即返回给前端的对象,VO可以仅向前端传输,页面需展示字段,如pageList等。
  • PO(Persistent Object):  持久化对象,就是对象是不是需要通过系列化持久化存储到数据库。 其作用就是项目的编码规约,比如我们把需要持久化保存的对象命名为PO.
  • Entity实体: 也可以用作DO,看项目的编码规约,比如我们使用Entity来做service直接的对象传递。

领域模型的后缀命名也是要结合团队工程规范:比如还有

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

Query/Filter/Param后缀:数据查询对象,各层接收上层的查询请求。

Form后缀:表单提交对象

领域模型类里面到底要不要写逻辑,还是只能用最普通的get/set方法。这就是讨论失血模型、贫血模型、充血模型的问题。

2、失血模型

失血模型:domain object只有属性的get set方法的纯数据类,所有的业务逻辑完全由Service层来完成的,由于没有dao,Service直接操作数据库,进行数据持久化。

service:  肿胀的服务逻辑

model:只包含get set方法

3、贫血模型

领域模型是指领域对象domain ojbect里只有get和set方法(POJO),所有的业务逻辑都不包含在内而是放在Service层。

service :组合服务,也叫事务服务

model:除包含get set方法,还包含原子服务(如获得关联model的id)

dao:数据持久化

贫血模型最早广泛应用源于EJB2,最强盛时期则是由Spring创造,将:

  • “行为”(逻辑、过程);
  • “状态”(数据,对应到语言就是对象成员变量)。

分离到不同的对象中:

  • 只有状态的对象就是所谓的“贫血对象”;
  • 只有行为的对象就是,我们常见的N层结构中的Logic/Service/Manager层(对应到EJB2中的Stateless Session Bean)。

——曾经Spring的作者Rod Johnson也承认,Spring不过是在沿袭EJB2时代的“事务脚本”,也就是面向过程编程。

贫血模型和面向对象设计背道而驰。但优点在于简单:

  • 对于只有少量业务逻辑的应用来说,使用起来非常自然;
  • 开发迅速,易于理解;
  • 注意:也不能完全排斥这种方式。

缺点无法良好的应对复杂逻辑。《重构-改善既有代码的设计》提到的坏代码味道之一就是纯数据类的问题,要求数据和行为是在一起,而贫血模型恰恰就是纯数据类的问题呢。

4、充血模型

面向对象设计的本质是:“一个对象是拥有状态和行为的”。

充血模型:绝大多业务逻辑都应该被放在domain object里面,包括持久化逻辑,而Service层是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。

service :组合服务 也叫事务服务

model:除包含get set方法,还包含原子服务和数据持久化的逻辑

它的优点是面向对象,Business Logic符合单一职责,不像在贫血模型里面那样包含所有的业务逻辑service太过沉重。

充血模型的问题也很明显,当model中包含了数据持久化的逻辑,实例化的时候可能会有很**烦,拿到了太多不一定需要的关联model。

三、项目顶层代码结构和调用链


1、 项目顶层代码结构

com  
 +- xxxCompany  
     +- myprojectApplication.java (应用启动主类)
     |
     +- myproject  
         |  
         +-common/base //公共/基础相关相关
         |  +-  AppConst.java  
         |  +-  AppContext.java  
         |   
         +-module1
         |  +---controller 控制层
         |  |   +- Userontroller.java
         |  |   
         |  +---model //模型
         |  |   +- domain 实体对象
         |  |   |  +-UserDO.java        
         |  |   |  
         |  |   +- dto 传输对象
         |  |   |  +-UserQueryDTO.java
         |  |   |  
         |  |   +- vo 前端对象
         |  |   |  +-UserProfileVO.java
         |  |   
         |  |   
         |  +---service//业务逻辑层,处理数据逻辑,验证数据
         |  |   +- impl
         |  |   |   +- UserServiceImpl.java          
         |  |   +- UserService.java
         |  |
         |  |
         |  +---dao //数据访问层,与底层 MySQL、 Oracle、 Hbase 等进行数据交互。
         |  |    +- UserDao.java
         |
         +- util//工具包
         |   +- DateUtils.java //Utils后缀 

2、调用链规约

我们项目编程规约,结合swaggger看:

1)controller层接收DTO请求参数 ,并进行简单参数校验。

2)controller层调用了Service层的接口方法。

3)Service层调用Dao层的方法,返回DO的entity对象。

4)Service层封装了前端需要VO对象,返回给controller层

5)controller层返回VO给前端。

四、常用命名原则和规约


规范命名约定目的是为了保持统一,减少沟通成本,提升团队研发效能。通过命名就能体现出代码的特征,含义或者是用途,让阅读者可以根据名称的含义快速厘清程序的脉络。做到见名知意,名副其实。

《Clean Code》这本书明确指出:

    好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。

    若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。

举个例子:

去掉下面复杂的注释,只需要创建一个与注释所言同一事物的函数即可

应替换为

1、首先遵循一定的原则:

好的命名风格应该是看到变量或者函数名字就能“望文生义”,做到名副其实。毕竟我们不能把自己写的所有代码都做注释。好的命名是不需要注释来补充的,达到代码自解释。

1)、编码规范统一:

在编写一个子模块或派生类的时候,要遵循其基类或整体模块的命名风格,保持命名风格在整个模块中的同一性。如骆驼命名法,大括号位置等。

2)、命名一致性原则:(命名的黄金原则)

    命名一致性:如booleam变量,前缀最好是is、can,need。

函数取名最忌讳"名不副实"

    比如一个 getXXX 的函数,结果里面还做了 add, update 的操作。对问题排查造成很大的困扰。因此命名一定要用实质内容相符。不论是类名,变量名,方法名都要有实际的意。   建议方法:先查查字典,找个通俗易懂而且比较贴近的名字。可以参考 jdk 的命名、通用词汇和行业词汇; 作用域小的采用短命名,作用域大的采用长命名。

  再比如有个Cache类,里面有个函数判断key是否过期:

public boolean isExpired(String key) {

        // 当前时间戳

        long curTimestamp = DateUtils.nowUnixTime();

        // 获取key的存入时间戳

        long storeTimestamp = getStoreTimestamp(key);

        

        if (curTimestamp - storeTimestamp > MAX_EXPIRE_SECONDS) {

            // 注意这个地方的delete是个隐藏逻辑

            delete(key);

            return true;

        }

        return false;

 }

  上面这个函数从函数字面意思看是判断key是否过期,但是!!它居然在函数里面隐藏了一段特殊逻辑:如果过期则删除掉key。这个就是典型的"名不副实",这个是最忌讳的,会给后续的开发人员留下"巨坑"。

  有两种方式去优化这段代码:

  • 方式一:将隐藏逻辑去掉

public boolean isExpired(String key) {

        // 当前时间戳

        long curTimestamp = DateUtils.nowUnixTime();

        // 获取key的存入时间戳

        long storeTimestamp = getStoreTimestamp(key);

        

        if (curTimestamp - storeTimestamp > MAX_EXPIRE_SECONDS) {

            return true;

        }

        return false;

 }

  • 方式二:改变函数名字

public int deleteIfExpired(String key) {

        // 当前时间戳

        long curTimestamp = DateUtils.nowUnixTime();

        // 获取key的存入时间戳

        long storeTimestamp = getStoreTimestamp(key);

        

        if (curTimestamp - storeTimestamp > MAX_EXPIRE_SECONDS) {

            return delete(key);

        }

        return 0;

 }

3)、方法名字标识符组成:动词+名词

1、名字标识符采用英文单词,应当直观且可以拼读,可望文知意,用词应当准确。正确的英文拼写和语法可以让阅读者易于理解,避免歧义。

2、杜绝完全不规范的英文缩写,避免望文不知义。如Function“缩写”成 Fu,此类随意缩写严重降低了代码的可阅读性。

4、只要命名合理,不要担心方法名称太长

首先需要保证命名有意义,只要命名合理,不要担心方法名称太长,但方法名称过长常常又意味着该方法干的事太多了,则需要思考是否可以拆分方法,这也反映了"职责单一"设计原则。

保证命名有意义的前提之下,尽量保证命名的简短,删除一些不影响表达的单词,或者采用缩写。举几个例子:

  1. ActivityRuleRepository.findActivityRuleById() 可以简写成ActivityRuleRepository.findById(),因为上下文已经说明白了这个一个查询活动规则的Repository接口。
  2. void updateRuleForRevision(String ruleString) 简写成void updateRule4Revision(String ruleStr)
  3. ActivityRule convert2ActivityRule(String ruleStr) 借鉴toString的简写方式,简写成ActivityRule toActivityRule(String ruleStr)

3)最小化长度 && 最大化信息量原则

在足够描述用途的前提下,尽量以最小词汇量去描述事物,如staff就company_person_collection要简捷易记。

也可采用有广泛共识的缩写后缀,如msg,idx,cnt,btn,id ,***等,如果缩写共识度不高,请在取得同事们共识后用注释说明其含义。

变量/函数长度控制在4~18个字符内,有助于对代码的理解。

过长的变量:howLongDoesItTakeToOpenTheDoor, howBigIsTheMaterial…

简捷的变量:timeToOpenTheDoor, materialSize.

4)避免过于相似,也不要用双关语,避免歧义,比如add和append:

不要出现仅靠大小写区分的相似的标识符,例如“i”与“I”,“function”与“Function”等等。

正确命名具有互斥意义的标识符

用正确的反义词组命名具有互斥意义的标识符 ,如

add / remove       begin / end        create / destroy

insert / delete       first / last         get / set

increment / decrement                 put / get

add / delete         lock / unlock      open / close

min / max          old / new         start / stop

next / previous      source / target     show / hide

send / receive       source / destination

cut / paste          up / down

这些有助于理解成对的变量/函数的意义.

尽量避免名字中出现数字编号

尽量避免名字中出现数字编号,如value1,value2等,除非逻辑上的确需要编号。

5)少使用类型前缀

最好从名字上就能揣测出其类型。加后缀说明是可以的。

少单独使用含义广泛的词。如data,info,value等。

6)避免过度使用get作为方法前缀

应该用更精确的动词描述动作,如“请求”request,“获取”acquire,“查找”search/lookfor/find,“查询”inquire,“构建”build 或“创建”create, 执行某个动作如执行查询doGet

get和set和成对出现,是轻量的实现。

get这种方法命名一定是明确index,性能比较好,

query选择选择符合的。

  1. 找到数据:find/lookfor,肯定有数据。
  2. 搜索数据:search、搜索数据可能不存在。
  3. 查询数据:query 强调查询这个动作,返回的数据是比较原始。query返回的是List<object>,find返回的才是List<Record>。

2.常见类名命名规范:

1、类应该是名词形式,通常由单个名词或名词短语组成,通常以名词结尾。而且在类名中要体现它是以保存数据为主还是提供功能为主。例如 ObjectBuilder 这个类我们都可以猜到它的主要功能是创建Object对象,

2、以动词-er/or 结尾的类名,至少应该包含一个以该动词开头的方法。例如 ObjectBuilder 这个类,它至少应该包含一个以 build- 开头的方法。有了这种规约,阅读者就能更方便地理解这个类。

前缀名

意义

举例

Abstract 或者 Base 开头 抽象类 BaseUserService

后缀名

意义

举例

Controller

对外接口类

UserController

Service

服务类,里面包含了给其他类提同业务服务的方法

UserService

Impl

实现类,而不是接口

UserServiceImpl

Manager

通用业务处理层

UserManager

Ixxx

接口

IUserDao

Dao

数据访问方法

OrderDao

Listener

响应某种事件的类

PaymentSuccessListener

Event

这个类代表了某种事件

PaymentSuccessEvent

Factory

生成某种对象工厂的类

PaymentOrderFactory

Adapter

用来连接某种以前不被支持的对象的类

DatabaseLogAdapter

Job

某种按时间运行的任务

PaymentOrderCancelJob

Wrapper

这是一个包装类,为了给某个类提供没有的能力

SelectableOrderListWrapper

DO

定义与数据库对象应的属性(ORM对象关系映射

UserDO

DTO

DTO数据传输对象

UserDTO

VO

用于展示层,  前台(APP\WAP\PC)展示用

UserVO

Entity

实体

属性

约束

举例

设计模式相关类 Builder,Factory等

当使用到设计模式时,需要使用对应的设计模式作为后缀,如ThreadFactory。

Bridge:桥连模式
Builder:建造者模式
Adapter: 适配器
Decorator:装饰器
Delegation:委派模式
Facade: 外观模式
Proxy:代理模式
terator: 迭代器
Observer/Observable: 观察者模式
Strategy:策略模式
Command:命令模式

Util类 一般是无状态的,只包含静态方法。使用时无需创建类的实例。
Helper类 可以有状态(类的成员变量),一般需要创建实例才能使用。
处理特定功能的 Handler,Predicate, Validator 表示处理器,校验器,断言,这些类工厂还有配套的方法名如 handle,predicate,validate

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

接口管理:XXXManager
小讯
上一篇 2025-03-22 09:29
下一篇 2025-03-07 13:00

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/121184.html