注意,这是我的架构实践心得的第二季的系列文章,第一季有10篇你也可以回顾。
最近我一直在思考几个问题:
其实,这几个问题或多或少是相互关联的。有的时候大家也会自嘲说,“程序员接手的代码永远是烂摊子,然后自己继续在这个烂摊子上产出代码,留给又一波后人接手”。十几年来经历过十来个公司,我看了不少差的代码,也看了不少好的代码,自己产出过垃圾代码,也带领团队实现过一些自认为不错的代码。你可能会说,业务代码就是增删改查,和框架代码的难度不能比,完全是机械劳动,其实我觉得不完全是这样,甚至完全不是这样,我个人认为写出能跑的业务代码不难,但要写出好的业务代码其实是挺难的,更重要的是如果系统设计的足够好,在很长一段时间内系统的可维护性是可控的,只需要简单扩展即可,如果基础打的不够好,那么项目可能就是一次性项目,下面我列出业务系统我关注的一些点,你想想是不是有道理。
标准化
标准的项目结构
我自己非常注重搭建项目结构的起步过程,模块的划分、目录(包)的命名,我觉得非常重要,如果做的足够好,别人导入项目后可能只需要10分钟就可以大概了解结构了。
1、有些名词是约定俗成的,大家一眼就能看出是啥东西的,比如:
比较重要的是确定先进行分类再分业务,还是先分业务再分类,在代码里混用这两种风格的结构就会很混乱:
对于直筒的三层架构的纯数据表驱动的代码我建议第一层是分类,第二层是业务功能:
对于有一些项目,不一定每一个逻辑都涉及到三层架构,数据流量比较复杂,我建议是按照业务功能先来分,下一层视情况也不一定完全是需要按照组件类型分二级目录,也可以是按功能来分:
对于这种目录结构一眼望去就能知道大概项目数据流和架构了,core对内,dispatcher做分发的感觉,callback是外部来的回调数据,notification是通知外部的数据流。这种数据流向复杂的项目,使用这种结构会比前一种合理的多,因为我们需要先关注数据流,而不是三层结构的层次,甚至对于core、dispatcher、notification我们知道其实是没有controller的。
2、有些名词可能就需要内部有一个统一,比如不同的层次面向数据库,面向业务,面向UI,面向RPC需要有不同的POJO,我们需要明确一套对应的命名,能明确就好,比如下面的这些POJO我们其实挺难分辨其用途的,需要进行规范,并且放置于匹配的目录结构中:
我们可以约定第一组用于服务本身访问外部(的Rpc服务也好,REST服务也好),第二组用于服务本身对外提供的Web Api,比如:
总之,虽然可能10 人在维护相同的项目,目录结构的风格、命名、专用名词的使用一定要统一。
统一的框架
这个需要在开展项目之前明确下来,我见过有项目中同时使用了Spring MVC和Jersey做Web API,同时使用了Spring Scheduler和Quartz做任务调度。最好是项目开展前明确框架的版本并且搭建好项目脚手架,大概涉及:
当然,我们也可以独立出依赖管理的项目,专门由独立模块进行依赖版本管理。最差也要在Wiki上进行明确。
统一的API
如果项目涉及到对外提供API,那么非常有必要在初期就规范API的框架定义,涉及到:
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的格式还涉及到安全处理、版本处理、客户端操作方式等等。对于一些需要服务端驱动客户端的业务(UI逻辑动态)来说,我们可以定义一套更复杂的ApiResult,让服务端告知客户端这个时候应该是提示还是跳转还是返回等等。
统一的源码工作模式
现在大家都使用Git,分支如何管理每一个公司(在Gitflow的基础上)都会略有不同,也需要和大家明确:
别小这个,虽然这个和代码质量和架构无关,但是梳理清楚可以:
容错性
见过一些项目在实现业务代码的时候是不考虑任何异常处理、事务处理、锁处理的,在流量小无并发的情况下,这些项目不容易爆发出严重的问题,基本能用。但是对于高并发的项目或将来可能会高速发展的项目,如果不考虑这些问题会死的很难看。
我们来想一下,如果现在在设计一个订单服务,如果因为网络问题、并发问题导致数据错乱、服务中断的可能在千分之一,如果一个业务一天只有1000次请求,1天才遇到这样1次问题,即使遇到了问题用户也不一定会来反馈,即使来反馈往往客服也能通过后台取消订单等操作来处理,这个问题不会爆发出来,如果一天的单量是1000万,那么每天可能就会有10000单异常的订单,这个可能就超过了客服的处理能力了。
很少有项目真正100%完全做好了所有细节,只不过往往是因为量小得不到大家的重视罢了。但我们想一下,如果遇到数据库或中间件级别大规模故障的情况下,如果一致性处理的不好,那么数据库恢复后可能会留下一大堆异常数据需要修复,如果处理的好,业务数据不会错乱,数据库恢复后业务马上可以恢复。在遇到事故的时候,系统这方面的设计功力就体现出来了。
一致性处理
在实现代码的时候需要考虑如下事情:
这些要做好真的很难,每一步都需要认证考虑,但是很遗憾见过的很多具有复杂业务的代码,在Service中一连串调用了N个外部服务进行写操作,方法内也实现了N个表的写操作,即不考虑外部服务的事务和补偿问题,本地也没有事务控制,出了错只是打印了堆栈然后客户端看到的是一个服务器忙。
异常处理
异常处理不仅仅是狭义上遇到了Exception怎么去处理,还有各种业务逻辑遇到错误的时候我们怎么去处理。
就拿记日志这一件事情来说:
我们知道catch到了异常或遇到了业务错误,我们除了记录日志还有很多选择,也需要认真考虑什么时候应该做什么:
这又涉及到了弹力设计的话题,我们的系统往往会对接各种外部服务、Api,大部分服务都不会有SLA,即使有在大并发下我们也需要考虑外部服务不可用对自己的影响,会不会把自己拖死。我们总是希望:
架构设计
表
表的设计和Api的定义类似,属于那种开头没有开好,以后改变需要花10x代价的,我知道,一开始在业务不明确的情况下,设计出良好的一步到位的表结构很困难,但是至少我们可以做的是有一个好的标准:
除了标准,尽可能需要结合业务以及业务可能的扩展思考一下:
对于表结构文档,我觉得列出字段类型、长度、说明是不够的,如果能结合业务代码梳理清楚下面这些,那这个文档就是真正有价值的表结构文档:
设计模式
我想90%的业务项目都是所谓的三层结构,Web层处理参数调用Service层做Db层的聚合,Db层基本就是代码生成或Orm框架补充少量的手写SQL。对于这样的项目,大部分人认为是没有设计的,也不需要设计。我认为那是因为没有好好思考:
说白了就是利用各种设计模式和OO思想,来尽可能在业务变化需要扩展的时候:
往大了说
在一个公司层面,如果有几十个,几百个业务项目,我们看这个公司的技术水平到了什么程度,我个人认为不仅仅是用了什么新技术,而是是否:
最简单的一个例子,一个业务从前到后跨10个事业部,100个服务,实现灰度测试,想想这件事情有多难?整个公司层面要实现步调一致的这些东西还确实很难,不仅仅是技术能力的体现,没有良好的组织架构,人心不齐,恐怕这些无法实现,实现了也无法推广,推广了也无法持续……当然,这些已经超出个人能做的了,作为程序员的我们应该从我做起,认真考虑前面提到的这些问题,至少在项目内部做良好的设计。
再来看看文首的问题,你看,虽然只是写业务代码,如果要写的足够好,必须要了解设计模式、理解各种弹力设计、理解事务、熟悉框架、了解中间件原理,怎么可能学不到东西,要实现健壮的业务代码,其实很难,要考虑的东西太多了,如果说写框架我们需要考虑不同的使用方和使用环境,这很难,写业务代码我们要考虑到千奇百怪的使用行为,要考虑到层次不起的对接方,这不比写框架简单。对于5年 经验丰富的程序员应当有能力开一个好头,或者说愿意在老代码上去做一些改变,否则你的价值在哪里呢?
本文只是展开了一些想到的内容,每一点都有很多东西可以写,也没时间一些子展开说太多,这些细节留在今后的文章慢慢展开了。
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved