内容简介:最近我一直在思考几个问题:其实,这几个问题或多或少是相互关联的。有的时候大家也会自嘲说,“程序员接手的代码永远是烂摊子,然后自己继续在这个烂摊子上产出代码,留给又一波后人接手”。十几年来经历过十来个公司,我看了不少差的代码,也看了不少好的代码,自己产出过垃圾代码,也带领团队实现过一些自认为不错的代码。你可能会说,业务代码就是增删改查,和框架代码的难度不能比,完全是机械劳动,其实我觉得不完全是这样,甚至完全不是这样,我个人认为写出能跑的业务代码不难,但要写出好的业务代码其实是挺难的,更重要的是如果系统设计的足
注意,这是我的架构实践心得的第二季的系列文章,第一季有10篇你也可以回顾。
最近我一直在思考几个问题:
-
业务代码究竟难不难写?
-
一直开发业务代码是不是完全学不到东西?
-
5年+开发经验的老 程序员 的价值在哪里?
-
如何通过面试来区分业务代码开发的水平?
其实,这几个问题或多或少是相互关联的。有的时候大家也会自嘲说,“程序员接手的代码永远是烂摊子,然后自己继续在这个烂摊子上产出代码,留给又一波后人接手”。十几年来经历过十来个公司,我看了不少差的代码,也看了不少好的代码,自己产出过垃圾代码,也带领团队实现过一些自认为不错的代码。
你可能会说,业务代码就是增删改查,和框架代码的难度不能比,完全是机械劳动,其实我觉得不完全是这样,甚至完全不是这样,我个人认为写出能跑的业务代码不难,但要写出好的业务代码其实是挺难的,更重要的是如果系统设计的足够好,在很长一段时间内系统的可维护性是可控的,只需要简单扩展即可,如果基础打的不够好,那么项目可能就是一次性项目,下面我列出业务系统我关注的一些点,你想想是不是有道理。
标准化
标准的项目结构
我自己非常注重搭建项目结构的起步过程,模块的划分、目录(包)的命名,我觉得非常重要,如果做的足够好,别人导入项目后可能只需要10分钟就可以大概了解结构了。
1、有些名词是约定俗成的,大家一眼就能看出是啥东西的,比如:
-
controllers
-
services
-
configs
-
utils
-
commons
-
jobs
比较重要的是确定先进行分类再分业务,还是先分业务再分类,在代码里混用这两种风格的结构就会很混乱:
-
controllers
-
order
-
user
-
jobs
-
order
-
user
-
order
-
services
-
mappers
-
user
-
services
-
mappers
对于直筒的三层架构的纯数据表驱动的代码我建议第一层是分类,第二层是业务功能:
-
看一眼controllers目录我们知道项目对外的Api能力
-
看一眼services目录我们知道项目的逻辑复杂度
-
看一眼mappers目录我们知道项目的表结构
对于有一些项目,不一定每一个逻辑都涉及到三层架构,数据流量比较复杂,我建议是按照业务功能先来分,下一层视情况也不一定完全是需要按照组件类型分二级目录,也可以是按功能来分:
-
core
-
storage
-
service
-
dispatcher
-
engine
-
context
-
callback
-
gateway
-
handler
-
notification
-
sms
-
push
对于这种目录结构一眼望去就能知道大概项目数据流和架构了,core对内,dispatcher做分发的感觉,callback是外部来的回调数据,notification是通知外部的数据流。这种数据流向复杂的项目,使用这种结构会比前一种合理的多,因为我们需要先关注数据流,而不是三层结构的层次,甚至对于core、dispatcher、notification我们知道其实是没有controller的。
2、有些名词可能就需要内部有一个统一,比如不同的层次面向数据库,面向业务,面向UI,面向RPC需要有不同的POJO,我们需要明确一套对应的命名,能明确就好,比如下面的这些POJO我们其实挺难分辨其用途的,需要进行规范,并且放置于匹配的目录结构中:
-
CreateOrderRequest / CreateOrderResponse
-
CreateOrderParam / CreateOrderDto
我们可以约定第一组用于服务本身访问外部(的Rpc服务也好,REST服务也好),第二组用于服务本身对外提供的Web Api,比如:
-
controllers
-
queryOrder()
-
createOrder()
-
OrderController
-
QueryOrderParam
-
QueryOrderDto
-
CreateOrderParam
-
CreateOrderDto
-
rpcs
-
login()
-
register()
-
UserService
-
LoginRequest
-
LoginResponse
-
RegisterRequest
-
RegisterResponse
-
services
-
OrderService
-
OrderServiceImpl
-
OrderEntity
-
storages
-
OrderMapper
-
OrderModel
总之,虽然可能10+人在维护相同的项目,目录结构的风格、命名、专用名词的使用一定要统一。
统一的框架
这个需要在开展项目之前明确下来,我见过有项目中同时使用了Spring MVC和Jersey做Web API,同时使用了Spring Scheduler和Quartz做任务调度。最好是项目开展前明确框架的版本并且搭建好项目脚手架,大概涉及:
-
Web API / Web MVC
-
Job Scheduler
-
Micro Service
-
Config Center
-
Redis Client
-
Data Access
-
Entity Mapper
-
MQ Client
当然,我们也可以独立出依赖管理的项目,专门由独立模块进行依赖版本管理。最差也要在Wiki上进行明确。
统一的API
如果项目涉及到对外提供API,那么非常有必要在初期就规范API的框架定义,涉及到:
-
包装类
Result<T>
的定义(见过一个项目用了三种包装类的)以及遇到错误的情况下,Http状态码的体现 比如这样的包装类格式:
public class ApiResult<T> { boolean success; String code; String message; String path; long time; T data; }
我们可以这么和客户端的开发来明确:
1、即使遇到错误,Http状态码还是200,Http状态码如果是500或是404的话那一定是网关层面的错误了,这个错误不是后端服务返回的
2、在Http状态码还是200的时候代表收到了后端的返回,前端去按照ApiResult以Json格式反序列化Http Body的报文
3、然后查看success(如果没success也行,我们可以约定code是200就是成功),如果是success代表后端服务成功处理了请求,如果不成功,则根据后端给的错误代码映射表根据code进行处理或直接提示message中的内容。注意,这里的success只代表后端是否成功处理了请求,不代表请求代表的业务逻辑是否成功处理。举一个例子,如果这个请求是异步支付请求,那么success==true代表前端给的参数都正确,后端正确接受了支付申请,不代表支付成功
4、在success==true的情况下再去解析data中的内容,拿取客户端需要的信息,还是前面的例子,data里可以是 {"orderStatus":"PROCESSING", "orderId":"1234"}
,这个才是真正业务逻辑的数据和状态,success并不代表订单支付操作就是成功的,也可能是处理中的状态
所以这是几个层次的事情,Http Status->ApiResult.status->ApiResult.data.orderStatus
-
加解密规范和签名规范 Api的加密解密以及签名最好在设计的时候就考虑进去,而且要仔细斟酌,否则以后很难变更,特别Api的使用方是客户端的情况,客户端很难轻易强更。如果做SAAS服务,建议参考大厂的规范,比如亚马逊AWS的API规范或阿里云API的规范,不建议自己造轮子,大厂做的API规范都是经过安全方面的专家深度思考的。
-
版本管理规范(比如Url path路由还是Http header路由) 如果使用了老版本的话,是否需要在返回内容中提示新版本的Url、版本号、老版本最后维护时间呢?这个就不展开了
所以统一Api这个事情不仅仅是Api的格式还涉及到安全处理、版本处理、客户端操作方式等等。对于一些需要服务端驱动客户端的业务(UI逻辑动态)来说,我们可以定义一套更复杂的ApiResult,让服务端告知客户端这个时候应该是提示还是跳转还是返回等等。
统一的源码工作模式
现在大家都使用Git,分支如何管理每一个公司(在Gitflow的基础上)都会略有不同,也需要和大家明确:
-
分支的定义(master、develop、release、hotfix、feature)
-
分支命名规范
-
checkout、merge request流程
-
提测流程
-
上线流程
-
Hotfix流程
别小这个,虽然这个和代码质量和架构无关,但是梳理清楚可以:
-
提高开发和测试的工作效率,人多也乱
-
减少甚至杜绝代码管理导致的线上事故
-
让项目管理者和架构师可以明确什么代码现在在哪里
-
方便运维处理发布和回滚
-
让项目的开发可以灵活适应多变的需求
容错性
见过一些项目在实现业务代码的时候是不考虑任何异常处理、事务处理、锁处理的,在流量小无并发的情况下,这些项目不容易爆发出严重的问题,基本能用。但是对于高并发的项目或将来可能会高速发展的项目,如果不考虑这些问题会死的很难看。
我们来想一下,如果现在在设计一个订单服务,如果因为网络问题、并发问题导致数据错乱、服务中断的可能在千分之一,如果一个业务一天只有1000次请求,1天才遇到这样1次问题,即使遇到了问题用户也不一定会来反馈,即使来反馈往往客服也能通过后台取消订单等操作来处理,这个问题不会爆发出来,如果一天的单量是1000万,那么每天可能就会有10000单异常的订单,这个可能就超过了客服的处理能力了。
很少有项目真正100%完全做好了所有细节,只不过往往是因为量小得不到大家的重视罢了。但我们想一下,如果遇到数据库或中间件级别大规模故障的情况下,如果一致性处理的不好,那么数据库恢复后可能会留下一大堆异常数据需要修复,如果处理的好,业务数据不会错乱,数据库恢复后业务马上可以恢复。在遇到事故的时候,系统这方面的设计功力就体现出来了。
一致性处理
在实现代码的时候需要考虑如下事情:
-
本地事务处理:见过一些代码完全不考虑事务,或者是只是知其然使用
@Transactional
,但是方法内部完全catch了所有异常的情况 -
事务包含的方法块
-
嵌套事务、事务传播
-
什么时候遇到什么异常应该回滚
-
@Transactional
是否真正生效了? -
外部服务调用的事务问题
-
调用外部服务出现异常的时候,本地事务怎么处理
-
调用的外部服务是否允许重试(幂等调用)
-
调用外部服务出现未知结果后,怎么进行补偿
-
补偿是否有上限,是否存在死信数据卡死补偿的情况?
-
如果有2+外部服务连同本地数据库存储都需要有事务性,怎么实现
-
数据重复和顺序问题
-
先本地事务提交还是先调用外部接口(如果先调用外部接口,可能会遇到外部回调的时候本地事务还没提交找不到数据的情况)
-
从MQ收到的消息顺序问题怎么解决?
-
重新入MQ的延迟消息或重试消息乱序是否会有问题?
-
对外提供的Api或回调方法是否支持幂等?
-
锁的问题
-
哪个层面做锁?服务层分布式锁还是数据库层面锁?
-
乐观锁还是悲观锁?
-
你确信你的 Redis 锁方案是可靠的吗?
-
你是否知道多少请求再排队等待,又是为什么?
这些要做好真的很难,每一步都需要认证考虑,但是很遗憾见过的很多具有复杂业务的代码,在Service中一连串调用了N个外部服务进行写操作,方法内也实现了N个表的写操作,即不考虑外部服务的事务和补偿问题,本地也没有事务控制,出了错只是打印了堆栈然后客户端看到的是一个服务器忙。
异常处理
异常处理不仅仅是狭义上遇到了Exception怎么去处理,还有各种业务逻辑遇到错误的时候我们怎么去处理。 就拿记日志这一件事情来说:
-
WARN和ERROR的选择需要好好考虑,WARN一般我倾向于记录可自恢复但值得关注的错误,ERROR代表了不能自己恢复的错误。对于业务处理遇到问题用ERROR不合理,对于catch到了异常也不是全用ERROR。
-
记录哪些信息,最好打印一定的上下文(用户Id、订单Id、外部传来的关键数据)而不仅仅是打印线程栈。
-
记录了上下问信息,是否要考虑日志脱敏问题?可以在框架层面实现,比如自定义实现logback的
ClassicConverter
我们知道catch到了异常或遇到了业务错误,我们除了记录日志还有很多选择,也需要认真考虑什么时候应该做什么:
-
直接返回
-
抛出异常
-
重试处理
-
恢复处理
-
熔断处理
-
降级处理
-
甚至关闭业务
这又涉及到了弹力设计的话题,我们的系统往往会对接各种外部服务、Api,大部分服务都不会有SLA,即使有在大并发下我们也需要考虑外部服务不可用对自己的影响,会不会把自己拖死。我们总是希望:
-
尽可能以小的代价通过尝试让业务可以完成
-
如果外部服务基本不可用,而我们又同步调用外部服务的话,我们需要进行自我保护直接熔断,否则在持续的并发的情况下自己就会垮了
-
如果外部服务特别重要,我们往往会考虑引入多个同类型的服务,根据价格、服务标准做路由,在出现问题的时候自动降级
架构设计
表
表的设计和Api的定义类似,属于那种开头没有开好,以后改变需要花10x代价的,我知道,一开始在业务不明确的情况下,设计出良好的一步到位的表结构很困难,但是至少我们可以做的是有一个好的标准:
-
统一的附加字段,create_time,update_time,version等
-
表的命名标准,比如
[domain]_[tablename]_[tabletype]
-
字段类型、长度标准
-
虽没有外键,但是外表关联字段和主表字段的命名标准
-
_id还是_no等字段命名的区别
除了标准,尽可能需要结合业务以及业务可能的扩展思考一下:
-
1:N的可能性,是有1就足够了,还是一开始就要设计1:N的层次关系
-
如果表字段可能会很多,业务变化多,是否考虑1:1甚至1:N的扩展表,把扩展字段从主表分开
-
表的领域职责,表可能也会分上游、中游、下游,什么字段应该在哪里太重要了(我觉得表的领域相当于之前提到的项目结构中的包的分类,这个最好一开始定义清楚)
-
关联表字段冗余冗余到什么程度,冗余字段的同步
-
枚举的维护方式,是否考虑字典表?
对于表结构文档,我觉得列出字段类型、长度、说明是不够的,如果能结合业务代码梳理清楚下面这些,那这个文档就是真正有价值的 表结构文档 :
-
记录由哪个业务模块创建
-
数据重要程度
-
数据归档方案
-
字段数据源头
-
字段会由谁更改
-
字段可能会在哪里缓存
设计模式
我想90%的业务项目都是所谓的三层结构,Web层处理参数调用Service层做Db层的聚合,Db层基本就是代码生成或Orm框架补充少量的手写SQL。对于这样的项目,大部分人认为是没有设计的,也不需要设计。我认为那是因为没有好好思考:
-
在我们写下if-else的时候,我们就可以考虑使用抽象类+具体实现类的方式来替代
-
在实现层次化业务处理的时候,就可以考虑使用Filter或职责链模式来实现
-
在封装外部Api的时候与其每次都写一套解析逻辑,我们是否考虑进行动态封装呢
-
在数据改变后我们要记录改版轨迹,与其复制粘贴是否考虑过发布订阅模式
说白了就是利用各种 设计模式 和OO思想,来尽可能在业务变化需要扩展的时候:
-
只是新增代码而不是修改代码
-
尽可能减少重复代码复制粘贴
-
尽可能让同类代码都呆在一起
-
尽可能让直筒式的代码有层次
往大了说
在一个公司层面,如果有几十个,几百个业务项目,我们看这个公司的技术水平到了什么程度,我个人认为不仅仅是用了什么新技术,而是是否:
-
具有统一的开发、服务框架
-
具有统一的运维、监控、中间件、测试平台
-
具有清晰的纵向领域划分
-
具有清晰的横向基础平台服务和基础业务服务
-
具有统一的代码工作模式
最简单的一个例子,一个业务从前到后跨10个事业部,100个服务,实现灰度测试,想想这件事情有多难?整个公司层面要实现步调一致的这些东西还确实很难,不仅仅是技术能力的体现,没有良好的组织架构,人心不齐,恐怕这些无法实现,实现了也无法推广,推广了也无法持续……当然,这些已经超出个人能做的了,作为程序员的我们应该从我做起,认真考虑前面提到的这些问题,至少在项目内部做良好的设计。
再来看看文首的问题,你看,虽然只是写业务代码,如果要写的足够好,必须要了解设计模式、理解各种弹力设计、理解事务、熟悉框架、了解中间件原理,怎么可能学不到东西,要实现健壮的业务代码,其实很难,要考虑的东西太多了,如果说写框架我们需要考虑不同的使用方和使用环境,这很难,写业务代码我们要考虑到千奇百怪的使用行为,要考虑到层次不起的对接方,这不比写框架简单。对于5年+经验丰富的程序员应当有能力开一个好头,或者说愿意在老代码上去做一些改变,否则你的价值在哪里呢?
本文只是展开了一些想到的内容,每一点都有很多东西可以写,也没时间一些子展开说太多,这些细节留在今后的文章慢慢展开了。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 基于 CSE 的微服务架构实践:基础架构
- 典型数据库架构设计与实践 | 架构师之路
- 『互联网架构』软件架构-rocketmq之实践(62)
- HBase实践 | 数据人看Feed流-架构实践
- 架构实践全景图
- 微服务架构最佳实践
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java EE WEB开发与项目实战
李俊青 / 华中科技大学出版社 / 2011-11 / 59.80元
本书采用工程案例的形式,将日常Java EE项目开发所涉及的技术要点进行了解析,系统介绍了Apache的安装、Tomcat的安装、虚拟主机的配置、开发工具的搭配使用、验证码的使用、过滤器的使用、密码的加密与解密、JavaMail邮件发送、Web在线编辑器的使用、文件上传、数据库连接池、Ajax与Servlet的身份认证、Struts框架的应用、JSF框架的应用、Spring框架的应用、Hibern......一起来看看 《Java EE WEB开发与项目实战》 这本书的介绍吧!