来自:西格玛的博客 原文:http://lrwinx.github.io/2017/03/04/%E7%BB%86%E6%80%9D%E6%9E%81%E6%81%90-%E4
导语
自2013年毕业后,I ; 6 +今年已经是我工作的第4个年头了,总在做java相关的工作,终于有时间坐下来,写一篇关于java写法的一篇文章,来探讨一下如果你真的是一个java程序员,那你真的会写java吗?笔者5 x 6是一个务实的程序员,故本文绝非扯淡文章,文中内容都n j ? d = !是干货,望读者看后,能有所收获。
文章核心
其实,本不想把标题写的那么恐怖,只q # ^是发I 5 }现很多人干了几年java以后,都自认为是一个不错的java程序员了,可以( i l拿着o p v上万的工资都处宣扬r 6 T ]自己了,写这篇文章的目的并不是嘲讽和我一样做java的{ L A b T x同行们,只是希望读者看到此篇文章后,可以和我一样,心平气和的争取d ! p B g Z做一个优秀的程序n G p % J员。
讲述方向
由于一直从事移动互联网相关工作,java开发中经常和移动端打交道或者做Z { 9 7一些后端的工作,所以本篇文章更可能涉及和移动端的交互或者与后端的交互方式,笔者希望以自身的一些学习经验或者开发经r + d ? . j验,可以带动认真阅读本篇文章的读者们,让大家对java有一个更好的态度去学习它,它不只是一个赚钱的工具而已。
笔者身边有很多与笔者年龄相仿或年龄更2 b G p o大的朋友或同事,经常有人问我:“你现在还在学习吗?我觉得没什么好学的,这些东西都M 6 X 9 H差不多”,我总是回答只要有时间,我就要看一会书,这个时候,大家都会露出一副不屑的眼神或笑容。其实,非常! V + j { / 1 $ ?能理解身边朋友或同事的看法,以目前状态来讲,大多都是工作至少5年的程序员了,对于公司大大小小的业务需要c D f,以目前的知识储备来讲,都可z h q u m以轻松应对,“没有什么好学的”其实这句话没有多大的问题,但是,如果你对编程还有一点点兴趣,只是不知道如何s + 0 / `努力或改进,希望本篇文章可以帮到你。
技术点
本O K 5 @ C文不是一个吹嘘的文章,不会讲很多高N } W w =深2 c 3 4 8 & / &的架构,相反,会讲解很多基础的问题和写法问题,如果读者自认为基础问题和写法问题都是不是问题,那请忽略这篇文章,节省出时间去做一些有意义的事情$ ` t。
开发工具
不知道有多少”老”@ M ( ; w H s程序员还在使用eclipse,这些程序员们要不就是因% W j c ~ / {循守旧,要不就是根本就不知道其他好的开发工具的存o 0 Y ]在,eclipse吃内存 . 4卡顿的现象以及各种偶然莫名异常的出现,都告知我们是时候寻找新的开发工具了。
更换IDE( B 5
根本就不想多解释要换什么样的IDE,如果你想成为一个优秀的java程序员,请更换int4 * ? t 4 5ellij idea. 使用idea的好处,请搜索谷歌。
别告诉我快捷键不好用
更R _ J { i A W换IDE不在我本文的重点内容中,所以不下想用太多的篇幅去写为什么更换IDE,请谷歌。E l ^ w Q
在这里,我只能告诉你,更换? d O L nIDE只为了更好、更快的写& ^ A p 4 f A D J好L b ? ] ? D Ujava代码。原因略& d B e H q z (。
别告诉我快捷键不好用,请尝试新事物L G H ^。
bean
bean是我们使用最多的模型之一,我将以大篇幅去讲解bean,希望读者好好体? k = @ ;会。
domain包名
根据很多java程序Y 2 6员的”经验”来看,一个数据库表则对k u [ X p D Z t应着一个domain对象,所以很多程序员在写代码时,包名则使用:com.xxx.domain ,s ` }这样写好像已经成为了行业的一种约束,数据库映射对象就应该是domain。但是你错了,domain是一个领域对象,往往我们再做传统java软件web开发中,这些domain都是: b H $ & _ o _贫血模型,是没有行为的,或是没有足够的领域模型的行为的,所以,以这个理论来讲,这些d$ n G / 4 z ~ Lomainl g d E X都应该是一个普通的entity对象,并非领域对象,6 i 6 P m C 7 f所以请把包名改为:com.xxx.entity。
如果你还不理解我说的话,请看一下Vaughn Vernon出的一本叫做《IMPLEME , W c # SENTING DOMAIN-DRIj g L f * 9 v ,VEN DESIGN》(实现领域驱动设计)这本书,书中讲解了贫血模型与领域模型的区别,相信你会受益匪浅。
DTO
数据传输我们应该使用DTO对象作为传输对象,这是我们所约定的,因为很长时间我一直都在做移动端api设计的工作,有很多人告诉我,他们认为只有给手机端传输数据的时候(input or! ~ [ p k + | % output),这些对象称为DTO对^ j Q S w象。请注意!这种理解是错误的,只要是a ! G t用于网络+ % f Z n传输的对象,我们都认为他们可以当做是DTO对象,比如电商平台中,用户进行下单,下单H . f ~ $后的数据,订单会发到OMS 或O { . N j w者 ERP系统,这些对接的返回值以及入参也叫DTO对象。
我们约定某对象如果是DTO对象,就将名称改为XXDTO,比如订单下发OMS:OMSOrderInputDr T X h ) W TO。
DTO转化
正如我们所P t * - 9知,DTO% ! = g :为系统与R ! Q外界交互的模型对象,那么肯定会有一个步骤是将DTO对象转化为BO对象或者是普通的entity对象,让service层去处理。} : n r - ` W c +
场景g G [
比如添加会员操作,由于用于演示,我只考虑用户的一些简单数据,当后台管理员点击添加用户时,只需要传过来用户的姓名和年龄就可以了,后端接m f d受到数据后,将添加创建时间和更新时间和默认密码三个字段,然后保存数据库。
@RequestMapping(\"/v1/api( a c c @ J/user\")
@RestController
public class Userj M ~ s , (Api {
@Autowired
private UserService userSa n / A 8ervice;
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new User();
user.setU0 L v Dsern^ g # ^ Q r # g Eame(userInputDTO.getUsername());
use2 _ { 9r.setAge(userInputDTO} q 1.getAge());
return userService.addUser(user);
}
}
我们只关注一下上1 * f Q述代码中的转化代码,其他内容请忽略:d ; s [ - O
User user = new User();
user.v 5 w z e IsetUsername(userInputDTO.getUsername());
user.setAge(userInputDT( ~ i 7 p l R | ;O.getAge());
请使用工具
上边的代码,从逻辑上讲,是没有问- z t题的,只是这种写法让我很厌烦,例子中只有两个字段,如果有20个e 3 C字段,我们要如何做呢? 一个一个进行set数据吗?当然,如果你这么做了,肯定y * t + | J z w不会有什么问题,但是,这肯定不是一个最优的做法。
网上有很多工具,支持浅拷贝或深拷贝的Utils. 举个例子p w Z = y Z = y,我们可以使用orG 0 v ; eg.springframework.beans.BeanUtils#copyProperties对代码进行重构和优化:
@PostMa+ u I apping
public User addUser(UserInputDTO userInputDTO){
User user = new User();
BeA D , R 2 ranUtils.copyProperties(userInputD& ) _ rTO,userI ! ~ V X & 2 + .);
return userService.aM . 2 u : * B ? (ddUser(user);
}
BeanUtils.copD T qyProperties是一个浅拷贝方法,复` U ? 4 2 1 : H制属性时,我们只需要把DTO对象和要转化的对象两个的属性值设置为一样的名称,并且保证一样的类W 6 , { o + ) y型就可以了。如果你在做DTO转化的时候一直使用set进行属性赋值,那么请尝试这种方式简化代码,让代码更加清晰!
转化的语义
上边的转化过程,读者看后肯定觉得优雅很多,但是我S Z c o s X @ d S们再写java代码时,更多的需要考虑语义的操作,再看上边的代码:
User user = new Useh q ^ & @r();
BeanUtils.copyProperties(userInputDTO,user);
虽然这段代码很好的简化和优化了代码,但是他的语义是有问题的,我们需要提现一个转化过程才好,所p F B k :以代码改成如下:
@PostMapp3 o R Q # 9 4 f wing
public User addUser(UserInputDTO userInputDTO){
User user = convertFor(userInputDTO);
return userService.addUser(user);
}
private User convertFor(Usz k / F U R zerInputDTO useH } X 4 5 6 9 ; [rInputDTO){
Us/ $ ner usei # i - 5r = new User();
BeanUtils.copyProperties(u9 , L # k ]serInp D cutDTO,& o m G F I c - ?user);
return user;
}
这是一个更好的语义写法,虽然他麻烦了些,但是可读性大大增加- ( P ; I了,在写代码时,我们应该尽量把语义层次差不多的放到一个方法中,比如:
User use] : x 3 , c G 9 rr =A k q _ = C F | convertFor(userInputDTO);
r| q ~ f D 8 * &eturn userSM + ] ~ 5ervice.addUser(user);
这两段代码都没有暴露实现,都是在讲如何在同一个方法中,做一组相同层次的语义操作,而不是暴露具体的实现。
如上所述,是一种重构方式,读者可以参考Martin Fowler的《Refactoring Imporving the Design of Existing Code》(重构 改善既有代码的设计) 这本书中的Extract Method重构方式。
抽象接口7 L m w定义
当实际工作中,完成了几个api的DTO转化时,我们会发现,这样的操作有很多很多,那么应该定义好一个接口,让所有这样的操作都有规则的进行。如果接口被定义以后,那么convertFor这个方法的语义将产生{ H c {变化,他将是一个实现类。
看一下抽象后的接口:
public interface DTOConvert<S,TU E & Q ; 5 K> {
T convert(S s);
}
虽然这个接口很简单,但是这^ z J y里告诉^ % 1 K :我们一个事情,要去使用泛型,如果你是一个优秀的java程序员,请为你想做的抽象接口,做好泛型吧。
我们再来看接口实现:public classj , W j 0 H H b UserInputDTOConvert implements DTOConvert {
@Override
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyProperties(userInputDTO,user);
return user;
}
}
我们这样重构后,我们发现现在` . b } P 0 7的代码是如此的% / + ] a简洁,并且那么的规范:
@RequestMapping(\"/v1/api/user\")
@RestController
publD D M _ v Sic class UserAp/ h 3 b o R Ri {
@Autowired
private UserService userSerY g c o { ) Wvice;
@PostMapping
public User addUser(UserInputDTO userInputDTO){
User user = new UserInputDTOConvert().convert(userInputDTO);
return userService.addUser(user);
}
}
review cT v ;ode
如果你是一个优秀的java程序员,我相信你应该和我一样,已经数次重复review过自己的代码很多次了。我们再看这个保存用户的例子,你将发现,apip Q # D X C中返回值是有些问题的,问题就在于不应该直接返回U^ H $ U L Q |ser实体,因为如果这样q : / ] + + a E的话,就暴露了太多实体相关的信息,这样的返回值是不安全的,所以我们更应该返回一个DTO对象,我们可称它为UsD + b T s $ _erOutputDTO:
@PV F q O D i f } sostO # 0 X $ D oMapping
publ{ E Y , w vic UserOutputDTO addUser(UserInputDTO userIa e wnputDTO){
User user = new UserInputDTOConvert().convert(userInputDTO);
User ss ; ] O v `aveUe u ~ i k c ^serReW Y * - R C + zsult = userService.addUser(user);
UserOutputDTO result = new UserOutDTOConvert( l y . & J).convertToUser(saveUserResult);
return rese ~ 8 b Bult;
}
这样你的api才更健全。
不知道在看完这段代码之后,读者有是否发现d 2 y O P 5 a B Z还有其他问题的存在,作为一个优秀的java程序员,请看一下这段我们刚刚抽象完的代{ t Z - M %码:User user = new UserInputDTOConvert().convert(+ ] b Z duserInp8 Q F M D g J butDTO);你会发现,new这样一个DTO转化对象是没有必要的,而且每一个转化对象都是由在遇到DTO转化的时候才会出B g K A J * 2现,那我们应_ $ B ?该考虑一下,是否可以将这个类和DTO进行聚合呢,看一下我的聚合结果:public class UserInputDTO {private String username;private intl g m % | q age;
public String getUsername() {
return username;
}
public void setUsername(k : LString username) {i R + 2 h 2 H
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public User convertToUE A F w C Eser(){
UserInputDTOConvert userInputDTOConvert = new UserInputDTOConvert();
User convert = userInputDTOC@ 5 8 !onvert.c4 1 b W Monvert(this);
return con, q 0 C I % s - 5vert;n / ? j k
}
private static class Use@ Q BrInputDTOConvert implements DTOConvert<UserInputDTO,User> {
@OverrF Wide
public Us^ . t per convert(UserInputDTO userInputDS A , A J J t STO) {
User user = new User();
BeanUtils.copyProper0 q J X w R Gties(userInputDTO,user)A } Z;
return user;
}
}
}
然后api中的转化则由:User user = new UserInputDTOConvert().convert(userInputDTO);User saveUserResult = userService.addUser(user);
变成了:User user = userInputDTO.convertToUser();User saveUserResult = userSerP q ~ r zvice.addUser(user);
我们再DTO对象中添加了转化的行为,我相信这样的操作可以让代码的可读性变得更强,并且是符合语义的。
再查工具类
再来] , { : K看DTO内部转化的代码H A n . - $ y,它实现了我们自己定义的DTOConvert接口,但是这样真的就没有问题,不需a : . 4 & ~ F要再思考了吗?我觉得并不是,对于Convert这种转化语义来讲,很多工具类中都U F n ; - b有这样的定义,这中Convert并不是业务级别上的接口定义,它只是用于普通bean之间转化属性值的普通意义上的接口定义,所以我们应该更多的去读其他含有Convert转化语义的代码。我仔细阅读了一下GUAVA的源码,发现了cH m _ ~ o y H gom.google.common.base.Convert这样的定义:
publf f h d : S % 2ic abstract class Converter<A, B> implements Fd 5 d D Aunction<A, B> {
protected abstract B doForward(A a);
protected abstract A doBackward(B b);
//其他略
}
从源码可以了解到3 P M P { u 5,GUAVA中的Conve5 2 0 S W G SrV p _ H q Ot可以完成x D 8 a p A正向转化和逆向转化,继续修改我们DTO中转化的这段K } 5代码:
private static class UseM 3 c ] t @rInputDTOConve. } s ^ ; M U 5rt implements DTOConvert<UserInputDTO,User> {
@Overrin 0 7 j : B 9 K 5de
public User convert(UserInputDTO userInputDTO) {
User user = new User();
BeanUtils.copyPropertit w P J + & C ies(userInputDTO,user);
return user;
}
}
修改后:
private staticf R F C { O ! b p class UserInputDTOConvert extends. x y t Converter<UserInputDTO, User>8 ? v * {
@Override
po q @ protected User doForward(UserInputDTO userIj 9 rnputDTO) {_ 6 8
Usel h b 0 V J rr user = new User();
BeanUtils.copyProperties(userInputD# [ z U w g h qTO,user);
return user;
}
@Override
protected UserInputDTO doBackward(User user) {
UserInputDTO userInputDT~ X ] U a M RO = new UserInputDTO();
BeanUtils.copyProperties(user, [ r HuserInputDT7 - sO);
return userInz e d @putDTO;
}
}
看了这部分代码以后,你可能会问,那逆向转化会有什么用呢?其实我们有很多小的业务需求中,X ] D 4 l (入参和出参是一样的,那么我们变可以轻松的进行{ Z j ` 6转化,我将上边所提到的UserInputDTO和UserOM b q S 4 Z + autputDTO都转成UserDTO展示给大家:
DTO:
pub4 x J clic class UserDTO {
private String userna6 G x !me;
private int agt z s p # = $ w ue;
p[ o M {ublic StringB 3 4 { a } 1 b ( getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public User convertToUser(){
UserDTOConvert userDTOConvert = new UserDTOConvert();
User cX u v p 0 - J Donvert =Q t b Q userDTOConvert.convert(this);
return convert;
}
public UserDTO convertFor(User user){
UserDTOConvert userDTOConvert = new UserDTOConvert();
UserD! 3 = u MTO convert = userDTOConvert.reverse().convert(user);
return convert;
}
private) ? e ( / % static class UserDTOConvert extends Converter<UserDTO, User> {
@Override
protected User doForward(UserDTO userDTO) {
User usZ T Ier = new User();
BeanUtils.copyProl y gperties(userDTO,user);
return user;
}
@Override
protected UseF , . @ k {rDTO doBackl Q y O e ` d e zward(User user) {
UserDTO userDTO = new UserDTO();
BeanUtE * A J h ! r dils.copyProperties(user,userDTO);
retuY m O ` .rn userDTO;
}
}
}
api:
@PostMappiM v 7 * g D A T )ng
public UserDTOd X R x g E / addUser(UserDTO userDTO){
User user = use- c L l ~ *rDTO.convertToUser();
Usg I Z 6 ( & ^ Cer saveResultUser = userService.a! X q .ddUser(user);
UserJ s A g u -DTO result = userDTO.convertFoz v k D _r(saveResultUser);
return result;
}D I y U 4 ; 0 l
当然,上_ + ~ ( t述只是表明了转化方向的正向或逆向: 7 q ~ . q { @ h,很多业务需求的出参和入参的DTO对象是不同的,那么你需要更明显的告诉程序:逆向G | J d D ?是无法调用的:
private sO D # w | Qtatic class UserDTOConvert 1 u 1 = L K m ]t extend- K 7 P 2s Converten T u # C . w ) `r<UserDTO, User> {
@Override
protected User do* Q @ u [ TForward(UserDTO userDTO) {
User user = new User();
BeanUtils.copyProperties(userDTO,q 9 v # 1 u guser);
return user;
}
@Override
protected UserDTO doBackward(User user) {
throw new AssertionError(\"不支持R 3 A K +逆向转化方法!\")8 N % };
}
}
看一下doBackward方法,直接抛出了一个断言异常,而不是业务异常,这段代码告诉代码的调用@ K x b + R =者,这个方法不是准你调用的,如果你调用,我就”断言”你调用错误了。
关于异常处理的更详细介绍,可以参考我之前的文章:如何优雅W u D M ; e l N U的设计java异常 ,应# r U该可以帮你更好的理解异常。
bean的验证
如果你认为我上边写的那个添加用户api写的已经非8 D ^ J u 3 o常完美了,那只能说明你还不是一个优秀的程序员。我们应该保证任何数@ * A Y T据的入参到方法体内都s V N是合法的。
为c X K ; O b I什么要验证
很多人会告诉我,如果这些api是提供给前端进行调用的,前端都会进行验证啊,你为什还要验证?其实答案是( L 0 n H B这样的,我从不相信任何调用我api或者方法的人,比如前端验证失败了,或者某些人通过一些特殊的渠道(比如ChI + d 7 f @arles进行抓包),直接将数据传入到我的api,那我h Y % k v X仍然进行正常的业务逻辑处理,那么就有可能产生脏数据!“对于脏数据的产生一定是致命”,这I Z 9 & D 9 l {句话希望大家牢记在心,* L 3 v t X再小的脏数据也有可能让你找几个通宵!
jsr 303验证
hibernate提供的jsr 303实现,我觉得目前仍然是很优秀的,具体如何使用,我不想讲,因为谷歌上你可以搜索出很多答案!再以上班的api实例进行说明,我们现在对D! q ; d b + 1 ITO数据进行检查:
public class UserDTO {
@NotNull
privat= d k ne String username;
@NotNull
private int age;
//其他代码略
}
apK z W * i验证:
@PostMapping
p3 M xubl0 T 6 ] o qic UserDTO addUser(@Valid UserDTO userDTO){
User3 t u user = userDTO.convertToUser();
User saveResultUser = userService.addUser(user);
UserDTO result = userDTO.convertFor(saveResultUser);
return result;
}
我们需要将验证结果传给前端,这种异常应该转化为一个api异常= = 8 | K [ W(带有错误码的异常)。
@PostMapping
publicj m B e 6 F H k UserDTO addUser(c c Q H@Valid UZ C P T T 6 lserDTO userDTO, BindingResult bindingResu& n ? ^ n K Jlt){
checkDTOParams(bindingResult);
User user = userDTO.convertToUser();
Userz d H ! 4 Q F saveResultUser = userService.addUser(user);
UserDTO result = userDTO.conver9 ] U m 2 - ^ H %tFor(saveResultUser);
return result;
}
private void checkDTOParams(Binding^ o H L 2 . Result bindingResult){
if(bindingResult.hasErrors()){
//throw new 带验证码的验证错误异常
}
}
BindingResult是Spring MVC验证DTO后的一个结果集,可以参考spring 官方文档
检查参数后,可以抛出一个“带验证码的验证错误异常”,具体异常设计可以参考如何优雅的设计java异常
拥抱lombok
上边的DTO代码,已经让我看的很累了,我相信读者也是一样,看到那么多的Getter和Setter方法,太烦躁了,那时候有什么方法可以r @ r简化这些呢。k R E ] i C z y请拥抱lombok,它会帮助我们解决一些让我们很烦躁的问题
去掉SetterM q X { a V q +和Getter
其实这个标题,我不太想说,因为网8 = S上太多,但是因为很t 2 q ,多人告诉我,他们根本就不知道lombok的存在,所以为了让读者更好的学习,6 U =我愿意写这样一个例子:
@Setter
@Getter
public class UserDTO {= [ k ( v / t
@NotNull
private String username;
@NotNull
private int age;
public User convertToUseru N / d 2 b y K(){
UserDTOConvert userDTOConvert = new UserDTOConvert();
User convert = userDTOConvert.convek C ; Y R h 3rt(this);
return convert;
}
public UserDTO convertFor(U{ # , . C C L :ser user){
UserDTOConve{ N F = ?rt userDTOConver4 E @t =B C ^ new UserDTOConvert();
UserDTO coA W u 8 nvert = userDTt s U ~ ~ L 0 ZOConvert.reverse().convert(user);
return convert;
}
private static class UserDTOConvert extends Converter<UserDTO, User> {
@Override
proB ] X h ptecti l , X a hed User doForward(UserDTO userY l vDTO) {
User uN 8 + 9ser = ne| G h / s 6 sw User();
BeaE Z z 6nUtils.copyProperties(userDTO,user);
retuL ` E I z Lrn user;
}
@Override
protected UserDTO doBackwa| E $ X (rd(User user) {
throw new A! R F FssertionError(\"不支持逆向转化方法w o V!\");
}
}
}
看到了吧,烦人的Getter和Setter方法已经去掉了。但是上边的例子根本不足以体现lombok的强大。我希m 8 6 b v : % C望写一些网上很难查到,或者很少人进行说明的lombok的使用以及在使用时程序语义上的说明。比如:@Data,@AllArgsConstructoZ : 6 ^r,@NoArgsConstructo^ b t f @ Z A , ur..这些我就不进行一一F q E T r ?说明了,请大家自行查询资料.
bean中的链G a l | e式风格
什么是链式风格?我来举个例子,看下面这个Student的bean:
public class Student {
private String name;
private int age;
public String getName()Z T s 4 {
return name;
}
public Student setName(Strin8 n R @ J L S -g name) {
this.name = name;
return this;
}
public int getAge() {
return age;
}
public Student setAge(int age) {
return this;
}
}
仔细看一下set方法,这样的设置便是chain的style,调用的时候,可以这样使用:
Student student = new Student()
.setAge(24)
.setName(\"zs\");
相信合理使用这样的链式代码,会更多的程序带来很好的可9 V ^ k 0 - & C =读性,那看一下如果使用lombok进行改善呢,请使用 @Accessors(chain = true),看如下代码:
@Accessors(chain = true)
@Set; . + e w jter
@Getter
public class Student {
private String name;
private int agJ # 8 | oe;
}
这样就完成了一个对于bean来讲很友好的链式操作。
静态构造方法
静态构造方法的语义和U E ? S q ) )简化程度真的高于直接去new一个对象。比如new一个List对象,过去的使用是这样的:
LisD # _ o I f e Pt<String> list = new ArrayList<>();
看一下guava中的创建方式:
List<String> list = Lists.newArrayList();
Lists命名是一种约定(俗话说:约定优于Z 7 g - &配置),它是指Li+ C Q +sts是List这个类的一个工具类,那么使用List的工具类去产生List,这样的语义是不是要比直接new一个子类来的更直接一些呢,答案是肯定的,再比如如果有一个工具类叫做Maps,那你是否想到了创建M} = ! n -ap的方法呢:
Has| s a ! :hMap<String, String>E M G / objectObjectHas$ B s 4 # L ihMap = Maps.newHashMap();
好了,如果你理解了b h P e b我说的语义,那么,你已经向成为java程序员更近了一步了。
再回过头来看刚刚的Student,很多时v . # Y候,我们去0 K G写Student这个bean的时候,他会有一些必输字段,比如Student中的name字段,一般处理的方式是将u , Q Gname字段包装成一个构造方法,M u Z 5 9只有传入name这样的构造方法,才能创建一个Student对象。
接上上边的静态构造方法和必传参数的构造方法,使用lombok将更改成如下写法(@RequiredAr^ M 3 {gsConr E _ t ) : | ?structor 和 @NonNull):
@Accessors(chain = true)
@Setter
@Getterm b S o _ O H _ W
@RequiredArgsConstructor(staticName = \"ofNameA u + U\")
public class StudU g ] ] , u 3ent {
@NonNull pr= 5 hivate String name;
priv3 D O / # Jate int age;
}
测试代码:
Student s. , ! 4 T ~tudent = Student.o% l S + Y . c YfName(\"zsA m A | 6\");
这样构建出的bean语义是否要比直接new一# m P g m个含参的构造方法(包含 name的构造方法)要好很多。
当然,看过很多源码以后,我想相信将静态构造方法ofName换[ { K v R L f l成of会先的更加简洁:
@Accessors(chain = true)
@Setter
@Getter
@RequiredArgsConstructor(staticN1 } Rame = \7 C c [ 4 ~"of\")
public class Student {
@NonNull pm z A Nrivate String name;
pr& - ` t # I # `iv/ 1 E r Y S yate int age;
}
测试代码:
Student student = Student.of(\"zs\");
当然他仍然是= + g + y M J 8 .支持链式调用的:
Student student = Student.of(\"zs\").setAge(24);
这样来写代码,真的很^ / C 5 w 1 j +简洁,并且可读性很f { W 4 * , d强。
使用builder
Bui. , m C ^lder模式我不想再多解释了,读者可以看一下o | $ : 0 . F $ 9《Head First》(设计模式) 的建造者模式。
今天其实D O j a要说的是一种变种的builderM K k o模式,那就是构建bean的builder模式,其实主要的思3 / u i { s / l想是带着大家一起看一下lombok给我们带来了什么。
看一下Student这个类的原始builder状态:
public class Student {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(Strip G W tng name) {
this.name = name;
}
public int getAgeE i M() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public static B& r |uilder builder(){
return new Builder()Z / a W m i k :;
}
public static class Builder{
private Strin/ ~ H : Gg nam[ T [ 4 4 p }e;
private inm g + M B ) et age;
public Builder name(String name){
this.^ , .name = name;
return thi] . c gs;
}
pubF p F jlic Builder age(int age){
this.age = age;
r; 5 @eturn this;
}
public Student build(){
Student student = new St4 ` U @ I } :udent();
student./ t { 0setAge(agk [ t f 2 . h m Xe);h Z 3 I O o M
student.setName(name);
return student;
}
}
}
调用方式:
Student student = Student.builder().name(\"zs\").age(24).build();
这样的builder代码,让我K # / ~ = & |是在恶心难受,于是我打算用lombok重构这段代码:
@Bun g { Uilder
public class Student {
private String name;
private int age& . ;
}
调用方式:
Student studen@ m . Y T i : 5 kt = Student.builder().name(\P w Z , Q ~ p 0 j"zs\").age(24).build();
代理模式
正如我们所知的,在程序中调用rest接口是一个常见的行为_ y s ^ ! ;动作,如果你和我一样使用过spring 的RestT: A q Femplate,我相信你会我和一样,对他抛出的非http状态码异常深恶痛绝。
所以我们考虑将RestTeC 8 P a cmplate最为底层包装器进行包装器& Z C )模式的设计:
public abstract clasi N = 1s FilterRestTemplate implements RestOperations {
pro* A L G 9 6 f )tected volatile RestTemplateh 8 R l p restTemplate;
protected FilterRestTemplate(RestTemplate restT9 @ % E l 0 v 5emplate){
this.restTemplate = restTemplate;
}
//实现RestOperat| ^ 4 ! wions所有的接口
}
然后再由扩展s u [ m ( [ p类对FilterResr ] V { YtTemplate进行包装扩展:
public cn G w #lass ExtractRestTemplate extends FilterRestTemplate {
private RestTemplate restTemplate;
ph 2 .ublic ExtractRestTemplate(RestTeh ) ~ t Z }mplate restTemplate) {
super(restTemplate);
this.restTemplate = restTemplate;
}
public <T> RestResponseDTO<T> postForEntityWithNoException(String url, Object request, Class<T> responseType, Object... uriVariables)
throws RestClientException {
RestResponseD/ J 0 u 3 C N s DTO<T&gA w dtR s U 0 _; restResponseS B h k d E PDTO = new RestResponseDTO<TB Y D>();
ResponseEntity<T> tResponseEnti5 o u c I z Sty;
try {
tResponseEntity = rest V 7 6 ) K $Template.postForEnti8 1 q P F f F k [ty(url, request, responseType, uriVariables);
restResponseDTO.setData(tResponseEntity.getBody(A ` 7 } 2));
restResponseDTO.setMessage(tResponseEntity.getStatusCode* ) s ^ m C().name());
restResponseDTO.setStatusCode(tRespZ A o ^ ; WonseEntity.getStatusCodeValue(x / 3));
}catch (Exception e){
restResponseDTO.setStatusCode(RestResponseDTO.UNKNOWN_ERROR);
restResponseDTO.setMessage(e.getMessage());r $ 0 q p
restResponseDTO.setData(J p ynull);
}
return restResponseDTO;
}
}
包装器ExtractRestTe4 ) m m . Nmplate很完美的更改了异常抛出的行为,让程序更具有容错性。在这里我们不考虑ExtractRestTemplate完成的功能,让我们把焦点放在FilterRestTemplate上,“实现RestOperations所有的接口”,这个操作绝对不是一时半会可以写完的,当时在重构之前我几乎写了半个小时,如下:
public abstraU b h q F C ~ Fct class FilterRestTN z [ 9 K : . sem{ J Jplate implements Resk 2 itOperations {
protected volatile RestTemplate restTemplate;
protected FilterRestTemplate(RestTemplate restTemplate) {
this.restTemplate =j l + S Z restTemplate;
}
@Override
p$ n Y K 8 p _ublic <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
return restTemplate.getForObju s Hect(url,responseTyu 2 H Z !pe,uriVariables);
}
@Ovk 4 3 , y W |erride
pf 6 % 8 !ublic <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
return restTemplate.getForObject(url,responseType,uriVaria} 8 v 3bles);
}
@Override
public <T> T getForObject(U5 y }RI url, Class<T> responseType)[ k P V d n P 0 r throws RestClientException {
return restTemplate.getForObj; s ^ q H Gect(url,responseType);
}
@Override
public <T> ResponseEntity<T. 3 7 N v 5&gR u 5t; getForEntity(String url, Class<T> responseType, Object.. 5 s | { { ~ L j. uriVariables)R K . & throws RestClientException {
return restTemplate.getForEy 7 3 u w ` Dntity(url,respoR 0 4 o dnseType,u- x @riVariables);
}
//其他实现代码略。。。1 G + 5 F Z P
}
我相信你看了以上代码,你会和我一样觉得恶心反胃,后B P 1 A : . ! @来我用lombok提供的代理注解优化了我的代码(@Delegate):
@AllArgsConstrj z v d O L ductor
public abstract class FilterRestTemplate implements RestOperations {
@Delegate
protected volatile RestTemplate restTemplate;
}
这几行代码完全# X ^ + C 3 t替代上述那些冗长的1 s #代码。是不是很简洁,做一个拥抱lombok的程序员吧。
重构
需求案例
项目需求
项目开发阶段,有一个关于下单发货的需求:如果今天下午3点前进行下单,那么发货时间是明天,如果今天下午3点后进行下单,那么发货时间是} 1 y [ p ` 7 & 3后天,如果被确定的时间是周日,那么在此时间上再加1天为发货时间。
思考与重构
我相信这个需求看似很简单,无论I u R ? ` a & }怎么写都可以完成。
很多人可能看到这个需求,就动手开始写Calendar或Date进行计算,从而完成需求。
而我给的建议是,仔细: { m考虑如何写代码,然后再去写,不是说所有的时间操作[ B x [都用Calendar或Date去解决,一定要看场景。
对于时间的计算我们要考虑joda-time这种类似的成熟时间计算框架来写代码,它会1 D q ~ a让代码更加简洁和易读。
请读者先考虑这个需求如何用java代码完成,或先写一个你觉得完成这个代码的思路,再来看我下边的代码,这样,你的收获会更多一些:
final DateTime DISTRIBUTIOF B 4 O F x 7N_TIME_SPLIT_TIME = new DateTime().withTime(15,0,0,0);
private Date calculateDistributionTi5 _ ( [ y O 1 % .meByOrderCreateTime(Date orderCreateTime){
DateTime orderCreate[ v K HDateTime = new DateTime(orderCreateTime);
Date tomorrow =q 7 @ w E F ; orderCreateDa; | lteTime.plusDays(1).toDate();
Date theDayAfterTomo9 T 1 8 l g Zrrow = orderCreateDateTime.plusDays(2).toDate();
return orderCr@ & ) Z seateDatZ [ J } ~eTime.isAfter8 L F A t(DISTRIBUTION_TIME_SPLIT_TII ) y 5 # q .ME) ? wrapDisz ` ` w ? Y P c }tributionTime(theDayAfterTomp | 7orrow) :[ S ) - n - { g 6 wB Z % C t ;rapDistributionTime(tomorrow);
}
private Date wrapDistributionTime(Date distributionTime){
DateTime currentDistri[ i M K 7 k -butionDateTime = new DateTime(distributionTime);
DateTime plusOneDay = currentDistributionDateTime: | V p.plusDays(1);
boolean isSunday = (DateTimeConstaq $ K $nts.SUNDAY == currentDistributionDateTime.getDayOfWeek());
return isSunday ? plusOneDay.toDate() : currentDistributionDateTime.toDate() ;
}
读这段代码的时候,你会发现,我将判断和有可能出现的不同结果都当做一个变量,最终做一个三目运算符的方式进行返回,这样的优雅和可读1 l 5 _ B $性显而易见,当然这样的代码不是一蹴而就的,我优化J T S }了3遍产生的以上代码。读者可根据自己的代码和我写的代码进行对比。
提高方法
如果你做了3年+的程序员,我相信像如上这样的需求,你很轻松就能完成,但是如果你想做一个会写java的y 1 G ~ ; S 6 # q程序员,就好好的思考和重构代码吧。写代码就如同写字一样,同样的字,大家都会写,但是写出来是否好看就不一定了。如果想把程序写好,就要不q . z断的思考和重构,敢于尝试l { { @ k,敢于创新,不要因循守旧,一定要做一个优k M {秀的java程序员。提高代码水平最好的方法就是有条理的重构!(注意:是有条理的重构)
设计模式
设计模式就是工具,而不是提现你是否是高水平程~ ) p C序员的一个指标。
我经常会看到某一个程序员兴奋的大喊,哪个程序哪个点我用到了设计模式,写的多么多么优秀,多么多么好。我仔细去翻阅的时候,却发现有很多是过度设` 0 g 5计的。
业务驱动技术 or 技术驱动业务
业务驱动技术 or 技术驱动业务 ? 其实这是一个一直在争论的话题,但是很多人不这么认为, 7 h d我觉得就是大家不愿意承认罢了。我来和大家大概分析一下作为一个jaB 6 A . ` ,va程序员,我们应该如何判断自己所处于的位m / C r 0 B g )置.
业务驱动技术:如果你所在的项目是一个收益很小或者甚至没有收益的项目,o 0 V M x O 9 0 P请不要搞_ ) l 8 I _ m其他创新的东西,不要驱动业务要如何如何做,而是要熟知业务现在的痛点是什么?如何才能帮助业务盈利或者让项目更好,更顺利的进行。
技术驱动业务:如果你所在的项目是一个很牛的项目,比如淘宝这类的项目,我可以在满足业务需求的情况下,和业务沟通,使用什么K 5 r U E i { 0 H样的技术能更好的帮助业务创造收益,比如说下单的时候要进队列,可能几分钟之后订单状态才能处理完成,但是会让用户有更流畅的体验e S e x ; 7 7 w,7 d , v e P @赚取更多的访问流量,那么我相信业务愿意被技术驱动,会同P ) G意订单的延迟问题,这样便是技术驱动业务。
我相信大部分人还都处于业务- [ 8 [ ` N驱动技术的方向吧。
所以你既然不能驱动业务,那就请拥抱业务变化吧。
代码设6 K 2计
一直在做java后端的项8 d t O { L F目,经常会有一些变动,我相信大家也都遇到过。
比如当我们写一段代码的时候,我们考虑将需求映射成代码的状态模式,突然有一天,状态模式里边又添加了很多行为变化的东西,这时候你就挠头J z } V 9 B了,你硬生生的将状态模式中添加过多行为和变化。
慢慢的你会发现这m ~ s D t = } t些状态模式,其实更像是一簇算法,应该使用策略模式,这时你应该已经晕头转向了。
说了这K ; l p Z么多,我 4 8 ` ~ { $ $ t的意思是,只要你觉得合理,就请将状态模式改为策略模式吧,所有的模式并不是凭空想象出来的,都是基于重构。
java编程中没有银] Y = 0 2 _ n弹,请拥抱业务变化,一直思考重构,你就有一个更好的代码设计!
你真的优秀吗k c T @ M?
真不好意思,我取了一个这么无聊的标题。
国外M G +流行一种编c x Q n { K T程方式,叫做结对编程,我相信国l : k T 7 L d [内很多公司都没有这么做,我就不在讲述结对编程带来的好处了,其实就是一边code review,一边互相提高的一个过程。既然做不) c n K到这个,那如何让自己活在a 9 & J自己的世界中不断提高呢?
“平时开发的时候,) A O做出的代码总认为是正确的,而且写法是完美的。”,我相信这是大部分人的心声,还回到刚刚的问! + j S 题,如何, ? x S ( H 7在自己的世界中不断提高呢?
答案就是:
- 多看成熟框架的源码
- 多回头看自己的代码
- 勤于重构
你真的优秀吗# K D? 如果你每周# N w都完成了学习源码,回头看自己代码,然后勤于重构,我认为你就真的很优秀了。
即使也许你只是刚刚入门,但是f Y一直坚持,你就是一个真的会写java代码的程序员了。
技能
UML
不想多讨论UML相关的知识,但是我觉得你如果真的` Y R _ ~ 8 F z {会写java,请先学会表达自己,UML就是你说话的语言,做一名优秀的java程序员,请至少学会这两种UML图:
- 类图
- 时序图
clean code
我认为保持代码的简洁和可读性是代码的最基本保证,如果有一天为了程序的效率而降低了这两点,我认为是可以谅解的,除此之外,没有任何K 7 r =理由可以让你任意挥霍你的代码。
- 读者可以看一下Robert C. Martin出版的《Clean Code》(代码整洁之道) 这本书
- 可以参考美团文章聊聊clean code
- 也可以看一下阿里的Java编码规范
无论如何,请保持你的代码的整洁。
linux 基础命令
这点其实和会写java没有关系,但是linux很多时候确实承载运行java, | N S ~ [ 4 [ [的容器,请学好linux的基础命令。
- 参考鸟哥的《Linux私房菜》
总结
java是一个大体系,今天讨论并未涉及框架和架构相关知识,只是讨论如何写好代码。
本文从写java程序的小方面一直写到大方面,来阐述了如何才能写好java程序,并告诉读者们如何才能提高自身的编码% 5 ; r ^水平。
我希望看到这篇文章的各位都能做一个优秀的java程序员。