AI智能
改变未来

写了10年JAVA代码,为何还是给人一种乱糟糟的感觉?


写了10年JAVA代码,为何还是给人一种乱糟糟的感觉?

无敌码农 无敌码农

接触过不少号称写了10多年代码的程序员,可经常还是会发现他们的代码给人一种乱糟糟的感觉,那么如何才能写出让同事感觉不那么乱的代码呢?

一、为什么要写这篇文章

在开篇之前先说明下为什么要写这篇文章?在Java的世界里MVC软件架构模式绝对是经典的存在(PS:MVC是一种软件架构方式并不只有Java有),如果你是在最近十年前后进入Java的编程世界,那么你会发现自己这些年似乎从来没有逃离MVC架构模式的牢笼,只不过换着使用了不同的MVC框架,如早期的Struts1、Struts2以及现在几乎一统江湖的Spring MVC(少数自行封装MVC框架的公司除外)。

而随着互联网技术的发展,特别是Ajax等富客户端技术的发展,前端技术逐步形成了一套体系,并且逐步从后端代码(如JSP)中剥离出来,从而形成了现在普遍流行的前后端分离模式(这也是一段时间内为什么前端工程师会出现大量需求的原因),而这也对传统的MVC模式产生了一点小的改变,因为现在基于Java的后端服务中很少会有大量处理复杂界面逻辑的代码出现,因此MVC中的V(View)这一层就逐步被各类前端技术所替代,如AngularJS、React等。

所以现在的Java服务端绝大部分情况下只是在处理M(Model)+C(Controller)的逻辑,而从概念上来看,好像Model代表的就是数据模型、而C则是一种控制层逻辑,所以很多人(甚至包括一些写了很多年Java代码的人)有时候都会被这个概念所迷惑而在Model和Controller层之间摇摆不定,在这里我们需要明确MVC模式中的M不仅仅代表的是数据模型,而是包括了数据模型之内的所有业务逻辑相关的代码,而C则是比较轻的,它被赋予只有处理输入/输出参数以及对该请求进行逻辑流程控制的职能,如果你的代码中对Controller层有过重的逻辑代码侵入,要知道这是不符合MVC架构规范的!

在MVC架构定义中,由于M代表了所有业务逻辑相关的代码,所以M是要重点设计和规范的,其代码的结构和规范直接决定了软件的可维护性及质量,从本质上来说就是如何进行"代码结构+软件设计原则+设计模式"的组合运用。当然上面只是一句话,而其内涵则是一件非常考验编程水平的事情。关于软件设计原则+设计模式的内容非常丰富也需要时间+经验的积累!而代码结构则是可以通过一定规范进行约定,结合Spring MVC框架至少我们可以写出层次结构尽可能一致的代码!

二、应用分层怎么搞?

事实上关于Java如何规范开发的问题,不同公司的规范略有不同,不过作为国内Java语言应用最为广泛的公司——阿里巴巴发布的《阿里巴巴Java开发手册》中对应用的分层结构已经做了比较合理的划分!这里作者并不想标新立异,只是在此基础上做更为详细的解释和说明从而让使用Spring MVC框架的同学能够更好地明确其分层的对应关系!

分层结构

以下分层结构基于Spring MVC框架,总体上与阿里巴巴开发手册应用分层方式一致,分层结构示意图如下:

在基于Spring MVC框架的开发中,Controller层作为服务的入口主要承担接收和转换由终端层或者其他服务发送的网络请求,并将其转化为Java数据对象,然后对数据对象进行参数合法性校验(如字段长度、类型、数值的合法性等等)。之后通过在Controller依赖注入对应Service层服务接口,并进行业务逻辑层方法调用,如果业务逻辑并不复杂(是否复杂判断标准可通过方法代码行数、条件逻辑复杂度以及站在旁者角度看看是否便于维护等指标进行判断)那么可以直接操作数据库持久层完成业务逻辑;而如果Service层方法写着写着发现非常的多,逻辑条件也比较多,并且每个条件所需要处理的代码量超过一定的规模,那么此时你就要考虑是否需要要对该方法进行优化了!

而关于优化的方式依据逻辑的复杂程度可以做不同等级的拆分,例如简单点可以拆分一个私有方法处理该方法中的某一部分逻辑,从而减少主业务方法的代码量。而如果该业务层方法后面对应的是一个庞大的逻辑,例如在交易支付系统中,Controller层定义了一个支付的入口服务,而进入Service层方法后根据不同的业务接入方、不同的支付方式及支付渠道,都需要进行大量不同逻辑的处理,那么此时就需要考虑对这些不同场景的业务逻辑进行类级别的拆分,如通过工厂模式拆分不同的支付渠道处理类逻辑,而对于公共的处理逻辑则可以通过抽象类定义抽象方法进行抽象。例如私有方法拆分代码示例:

@Overridepublic SearchCouponNameBO searchCouponNameList(SearchCouponNameDTO searchCouponNameDTO) {SearchCouponNameBO searchCouponNameBO = SearchCouponNameBO.builder().total(0).build();SearchResult searchResult;try {BoolQueryCondition boolQueryCondition = searchCouponNameListConditionBuild(searchCouponNameDTO);SearchBuilderConstructor searchBuilderConstructor = new SearchBuilderConstructor(boolQueryCondition);searchBuilderConstructor.addFieldSort(\"id\", SortOrderEnum.DESC);searchBuilderConstructor.setFrom(searchCouponNameDTO.getOffset());searchBuilderConstructor.setSize(searchCouponNameDTO.getLimit());searchResult = salesCouponEsMapper.selectCouponNameByCondition(searchBuilderConstructor);} catch (Exception e) {throw new SalesCouponNameException(SalesCouponNameErrorCode.COUPON_NAME_ES_QUERY_ERROR.getCode(),SalesCouponNameErrorCode.COUPON_NAME_ES_QUERY_ERROR.getMessage(),searchCouponNameDTO);}if (searchResult != null && searchResult.getHits().getHits().length > 0) {List<Integer> idList = getIdListFromEsSearchResult(searchResult);List<SalesCouponNamePO> salesCouponNamePOList = salesCouponNameMapper.selectByIdList(idList);List<SalesCouponNameBO> couponNameBOList = SalesCouponNameConvert.INSTANCE.convertCouponNameBOList(salesCouponNamePOList);searchCouponNameBO.setList(couponNameBOList);searchCouponNameBO.setTotal((int) searchResult.getTotalHits());}return searchCouponNameBO;}

在该Service入口方法中,需要根据从ES查询的分页ID去真实的MySQL中进行数据获取(ES数据存储不全,只是为了进行优化性能将分页逻辑放入ES),而在处理ES数据时,需要从ES数据结果集中抽象ID列表,对于这部分逻辑出于代码量的考虑,这里我们抽象一个Service层私有方法,如:

private List<Integer> getIdListFromEsSearchResult(SearchResult searchResult) {SearchHit[] searchHits = searchResult.getHits().getHits();List<Integer> idList = Arrays.asList(searchHits).stream().map(SearchHit::getSourceAsMap).map(o -> Integer.parseInt(String.valueOf(o.get(\"id\")))).collect(Collectors.toList());return idList;}

以上代码示例,本质上是一种最简单的方法抽象(别的语言叫函数),如果在代码量略大,但是逻辑本身复杂度还不是特别高的情况下,这种方式是最常用的!也是在你不知道怎么拆分,让代码不那么难以维护的一种非常有效的手段。

而工厂+责任链等也是业务层拆分常用的手段,此时需要基于Service层业务入口方法进行代码结构的二次拆分,在分层结构上这部分介于Service层和Dao层之间的代码称之为通用业务处理层(Manager)。关于这部分由于可以发挥空间非常大,很难有一套标准的答案,但作为一名优秀的程序设计者要时刻有抽象的思维,不管拆分得是否足够合理,至少要让你的代码不至于过于臃肿!这里我们将Service层拆分层次定义为以下三个等级:

  • 等级1:私有方法拆分;
  • 等级2:工厂+责任链运用(有效的类的拆分);
  • 等级3:高级设计模式(优雅的类的拆分);

分层领域模型约定

聊完分层结构接下来我们说一下分层领域数据模型的约定,注意这里的分层领域并不是指“DDD(领域驱动设计)模式”,而是对以上分层结构中各层之间交互数据对象的定义约定。在上述分层结构图中已经标识了DTO、BO、PO的使用范围(本规范只约定三种领域对象,事实上已经足够,并不需要搞的太复杂)。具体如下:


在Controller层接收网络请求数据后,由于Controller层并不需要处理额外的逻辑,所以大部分情况下直接将DTO对象传送给Service层;而Service层如果逻辑不复杂只是需要根据DTO的数据进行数据库操作,那么此时根据需要将DTO转换为PO进行操作,完成后由于大部分场景下Service的输出参数与输入DTO对象都存在差异,因此为了区分我们将Service层的输出数据对象统一定义为BO。
而Service层拆分时对于Manager层方法的输入/输出对象则统一为BO,包括Manager层操作第三方数据接口的数据对象转换也统一为BO。以上划分并没有什么特别的强制约定,而过分人为的去揣摩其含义本质上也没什么意义,只是大家共同遵守一个约定,这样代码风格看起来会更加统一一点。

三、如何保持代码的简洁性

作为一名对代码有追求的程序员,能少些一行代码就绝对不要啰嗦,而Java丰富的开源生态体系也给了我们这种懒惰很多便利,所以在编程的过程中其实是有很多工具可以帮助节省代码的。这里给大家分别介绍三种方式:

MapStruct

在前面介绍的分层结构中,无论是DTO到BO,还是BO到PO亦或BO到BO,都会有很多的数据对象转换的逻辑,传统的方法是需要通过一堆Setter方法来完成的,而高级一点的lombok包提供的@Builder注解也是需要你写一堆".build()"来完成数据的转换,这样的代码写到Service层中显然很浪费很多代码行,而MapStruct是一种更优雅的完成这件事的工具,使用方法如下:

项目pom.xml中引入依赖:

<!--MapStruct Java实体映射工具依赖--><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-jdk8</artifactId><version>1.3.1.Final</version></dependency><dependency><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>1.3.1.Final</version></dependency>

也需要在pom.xml引入一下Maven插件:

<!--提供给MapStruct使用 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId></plugin>

之后编写数据对象映射转换接口:

package com.mafengwo.sales.sp.coupon.convert;import com.mafengwo.sales.sp.coupon.client.bo.SalesCouponChannelBO;import com.mafengwo.sales.sp.coupon.client.dto.SalesCouponChannelsDTO;import com.mafengwo.sales.sp.coupon.dao.model.SalesCouponChannelsPO;import java.util.List;import org.mapstruct.Mapper;import org.mapstruct.Mapping;import org.mapstruct.Mappings;import org.mapstruct.factory.Mappers;/*** @author qiaojiang*/@Mapperpublic interface SalesCouponChannelsConvert {SalesCouponChannelsConvert INSTANCE = Mappers.getMapper(SalesCouponChannelsConvert.class);@Mappings({@Mapping(target = \"flag\", expression = \"java(java.lang.Integer.valueOf(\\\"0\\\"))\"),@Mapping(target = \"ctime\", expression = \"java(com.mafengwo.sales.sp.coupon.util.DateUtils.getCurrentTimestamp())\"),@Mapping(target = \"mtime\", expression = \"java(com.mafengwo.sales.sp.coupon.util.DateUtils.getCurrentTimestamp())\")})SalesCouponChannelsPO convertSalesCouponChannelsPO(SalesCouponChannelsDTO salesCouponChannelsDTO);@Mappings({})List<SalesCouponChannelBO> convertCouponChannelBOList(List<SalesCouponChannelsPO> salesCouponChannelsPO);}

以上方法的入参为源数据对象,而返回对象则为目标数据对象,如果两个对象的字段名称完成一致,那么其实是不需要进行任何单独映射的,直接 @Mappings({})即可;而如果映射对象之间字段名称有差异则可以通过@Mappings({@Mapping(target = "ctime", source = "createTime")})进行指定映射。而在业务层方法具体操作时使用方法如下:

//实体数据转换SalesCouponChannelsPO salesCouponChannelsPO = SalesCouponChannelsConvert.INSTANCE.convertSalesCouponChannelsPO(salesCouponChannelsDTO);

这样对象数据之间的拷贝将变得非常容易,从某种层面上看无论代码层次结构多么绕,至少数据对象之间的拷贝将不再是一件麻烦的事!

lambada表达式

在Java8种提供了lambada表达式,在Java8中如果操作List相关数据结构,如果能够使用lambada表达式也可以省一些代码,例如:

private List<Integer> getIdListFromEsSearchResult(SearchResult searchResult) {SearchHit[] searchHits = searchResult.getHits().getHits();List<Integer> idList = Arrays.asList(searchHits).stream().map(SearchHit::getSourceAsMap).map(o -> Integer.parseInt(String.valueOf(o.get(\"id\")))).collect(Collectors.toList());return idList;}

有关lambada表达式更多的用法,大家有时间可以多看看相关语法知识,这里就不再赘述!

tk.mybatis

在使用Mybatis框架作为数据库开发框架时,相比较于Hibernate或其他JPA框架,Mybatis具有较强的对原生SQL的支持能力,因而会显得比较灵活。但在大部分互联网系统中,对数据库的操作很多时候都是单表的操作,在这种情况下使用Mybatis也需要在Mapper代码和映射.xml文件中编写大量的SQL,而这些单表SQL本质上大同小异,完全可以通用化。

因此在Mybatis领域为了减少开发量很多项目会使用mybatis-generator插件生成一份完整的映射代码,但是这样的方式也会增加大量的无用代码,看起来并不是那么的简洁。而tk.mybatis则是考虑到了这个问题,可以兼顾对单表操作的便捷性(不需要再写额外的代码)、多表联合查询的灵活性以及代码的简洁性。具体用法如下:

项目pom.xml文件引入相关依赖:

<!--Mybatis通用Mapper集成--><dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>2.1.3</version><exclusions><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId></exclusion><exclusion><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId></exclusion></exclusions></dependency><dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId><version>4.1.3</version></dependency>

主类@MapperScan注解换成tk.mybatis的:

import org.springframework.boot.SpringApplication;import org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticSearchRestHealthIndicatorAutoConfiguration;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.web.servlet.ServletComponentScan;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.transaction.annotation.EnableTransactionManagement;import org.springframework.web.servlet.config.annotation.EnableWebMvc;//不要使用Mybatis原生注解,用tk.mybatis的import tk.mybatis.spring.annotation.MapperScan;import java.util.Date;@SpringBootApplication(exclude = {ElasticSearchRestHealthIndicatorAutoConfiguration.class})@ServletComponentScan@EnableDiscoveryClient@EnableWebMvc@MonitorEnableAutoConfiguration@MapperScan(\"com.mafengwo.sales.sp.coupon.dao.mapper\")@EnableTransactionManagementpublic class SpCouponApplication {public static void main(String[] args) {SpringApplication.run(SpCouponApplication.class, args);}}

编写映射接口,单表操作将不再需要额外定义操作方法及映射SQL代码,而是可以直接用tk.mybatis提供的通用方法,代码如下:

import com.mafengwo.sales.sp.coupon.dao.model.CouponNameScopeRelationPO;import org.springframework.stereotype.Repository;import tk.mybatis.mapper.common.Mapper;@Repositorypublic interface CouponNameScopeRelationMapper extends Mapper<CouponNameScopeRelationPO> {}

而在Mybatis SQL映射文件*.xml中单表也只需要定义简单的字段映射即可,而不在需要定义通篇的SQL代码了,如下:

<?xml version=\"1.0\" encoding=\"UTF-8\" ?><!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\" ><mapper namespace=\"com.mafengwo.sales.sp.coupon.dao.mapper.SalesCouponChannelsMapper\"><resultMap id=\"BaseResultMap\" type=\"com.mafengwo.sales.sp.coupon.dao.model.SalesCouponChannelsPO\"><id column=\"ID\" property=\"id\" jdbcType=\"INTEGER\"/><result column=\"NAME\" property=\"name\" jdbcType=\"VARCHAR\"/><result column=\"DESC\" property=\"desc\" jdbcType=\"VARCHAR\"/><result column=\"ADMIN_UID\" property=\"adminUid\" jdbcType=\"INTEGER\"/><result column=\"FLAG\" property=\"flag\" jdbcType=\"INTEGER\"/><result column=\"CTIME\" property=\"ctime\" jdbcType=\"TIMESTAMP\"/><result column=\"MTIME\" property=\"mtime\" jdbcType=\"TIMESTAMP\"/><result column=\"SCENEID\" property=\"sceneId\" jdbcType=\"INTEGER\"/></resultMap></mapper>

除以上工具外,在实际的开发过程中还有很多开源或通过自定义组件的方式能够让代码写的更简洁,大家可以保持探索!

四、Java程序设计原则与设计模式

构建复杂的软件系统只有遵循一定的设计原则并合适地运用相应地设计模式,这样的代码才不至于在复杂的逻辑中迷失方向。关于设计原则及设计模式的话题是一个需要时间打磨和反复历练的修行,因此这里只是为大家简单陈列,在Java程序设计时应该遵循的一些原则以及可用的设计原则,做到心中有剑!

设计原则

单一职责(一个萝卜一个坑)、里氏替换(继承复用)、依赖倒置(面向接口编程)、接口隔离(高内聚、低耦合)、迪米特法则(降低类与类之间的耦合)、开闭原则(对扩展开发、对修改关闭)。

设计模式

在Java领域,大概有23种设计模式,它们分别是:
创建型模式:单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式
结构型模式:适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式
行为型模式:模板方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式

以上这些模式或多或少在我们日常的编程中都会见到或者听过,但在平时能够用到的却并不多,很多原因在于目前Java领域的开发框架如Spring已经给我们做了很多的限定,而在大部分互联网系统中,编程模式又很固定。在多数情况下,工厂模式的运用就能搞定大多数业务编程场景,因此很多模式只有在很多中间件系统等基础软件中被使用得比较多。通过罗列上述设计模式,并不是要大家为了设计而生硬的使用设计模式,而是要努力向着“心中有丘壑,眉目作山河”目标境界前进!只有这样才能不至于日复一日的码砖生涯中,迷失自我,失去方向!

五、后记

随着时光的流逝,越来越多的程序员步入中年,写了10多年代码的人也越来越多,而行业的发展却在走下坡路,种种因素让越来越多的人感到焦虑!个人觉得作为一名程序员,我们的核心能力还在于代码,因此在日复一日的码砖生涯中不断修炼自己的代码能力才是关键!否则可能就会出现被年轻人鄙视了!

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » 写了10年JAVA代码,为何还是给人一种乱糟糟的感觉?