写了10年JAVA代码,为何还是给人一种乱糟糟的感觉?
在开篇之前先说明下为什么要写这篇文章?在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架构规范的!
二、应用分层怎么搞?
事实上关于Java如何规范开发的问题,不同公司的规范略有不同,不过作为国内Java语言应用最为广泛的公司——阿里巴巴发布的《阿里巴巴Java开发手册》中对应用的分层结构已经做了比较合理的划分!这里作者并不想标新立异,只是在此基础上做更为详细的解释和说明从而让使用Spring MVC框架的同学能够更好地明确其分层的对应关系!
分层结构
@Override
public 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;
}
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:高级设计模式(优雅的类的拆分);
分层领域模型约定
在Controller层接收网络请求数据后,由于Controller层并不需要处理额外的逻辑,所以大部分情况下直接将DTO对象传送给Service层;而Service层如果逻辑不复杂只是需要根据DTO的数据进行数据库操作,那么此时根据需要将DTO转换为PO进行操作,完成后由于大部分场景下Service的输出参数与输入DTO对象都存在差异,因此为了区分我们将Service层的输出数据对象统一定义为BO。
三、如何保持代码的简洁性
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>
<!--提供给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
*/
@Mapper
public 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);
}
//实体数据转换
SalesCouponChannelsPO salesCouponChannelsPO = SalesCouponChannelsConvert.INSTANCE
.convertSalesCouponChannelsPO(salesCouponChannelsDTO);
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;
}
tk.mybatis
<!--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>
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")
@EnableTransactionManagement
public class SpCouponApplication {
public static void main(String[] args) {
SpringApplication.run(SpCouponApplication.class, args);
}
}
import com.mafengwo.sales.sp.coupon.dao.model.CouponNameScopeRelationPO;
import org.springframework.stereotype.Repository;
import tk.mybatis.mapper.common.Mapper;
@Repository
public interface CouponNameScopeRelationMapper extends Mapper<CouponNameScopeRelationPO> {
}
<?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领域的开发框架如Spring已经给我们做了很多的限定,而在大部分互联网系统中,编程模式又很固定。在多数情况下,工厂模式的运用就能搞定大多数业务编程场景,因此很多模式只有在很多中间件系统等基础软件中被使用得比较多。通过罗列上述设计模式,并不是要大家为了设计而生硬的使用设计模式,而是要努力向着“心中有丘壑,眉目作山河”目标境界前进!只有这样才能不至于日复一日的码砖生涯中,迷失自我,失去方向!
五、后记
随着时光的流逝,越来越多的程序员步入中年,写了10多年代码的人也越来越多,而行业的发展却在走下坡路,种种因素让越来越多的人感到焦虑!个人觉得作为一名程序员,我们的核心能力还在于代码,因此在日复一日的码砖生涯中不断修炼自己的代码能力才是关键!否则可能就会出现被年轻人鄙视了!
推荐阅读: