分层架构最佳实践

概述

分层的目的是为了将某个功能的实现逻辑,根据一定规则拆分到各层次,从而降低各层的复杂度,保证代码的可读性和可维护性。

我当过大量实践总结,设计了如下图所示的分层规范:


image.png

该分层规范的核心思想是将业务和技术细节相分离,也即分层架构下的业务逻辑层和服务层的划分。

接下来我说下如何分离。

业务与技术相分离

其分离可通过如下表格来说明。

逻辑 数据 异常
业务 业务逻辑 业务数据 业务异常
技术 技术逻辑 技术数据 技术异常

技术逻辑

技术逻辑,即我们的一些技术实现手段性的代码。比如我们要实现一个关闭订单的业务动作,其技术实现手段可能就是将数据库的某个字段值从0更新为1的过程。

技术数据

技术数据,通常情况下可以和业务数据一样。典型有区别的就是状态类的数据。比如我们的订单状态,从技术数据角度,它是0表示正常,1表示关闭。而在业务数据角度,它应该是枚举类型,CLOSED表示关闭,OPEN表示正常的。

技术异常

技术异常,通常也是系统性的异常。例如网络超时,远程服务报错,代码bug导致的空指针等异常。此类异常的特点是不可枚举且无法避免。该类异常一定要打印异常日志用于排查问题。同时该异常最好不要抛给调用方,因为调用方也看不懂。如果调用方是客户端,将异常信息展示给了用户,则体验极差,用户会懵逼。

业务逻辑

业务逻辑,即我们实现某个功能的业务流程。

业务数据

技术数据这一段已做说明,这里不多说了。

业务异常

业务异常,通常是不满足某个业务前提条件而进行某个业务动作时,被拦截终止流程的异常。比如用户要关闭一个订单,但目前订单已经是关闭了的,则可以抛出一个业务异常:订单已经关闭,无需重复关闭!。

以上就是业务和技术相分离的思想。了解了这些我们就可以接下来学习各层的职责划分。

各层职责

数据传输层

用于实现与远程服务的通信。远程服务包括db、rpc服务端、kafka服务端、redis服务端等等

该层比较简单,通常不需要我们开发,我们常见的操作数据库的Mapper、调用http服务的httpClient,以及调用远程RPC服务的client包,都是作为数据传输层来看待。

异常情况:该层可能会因为网络超时,远程服务无响应抛出系统异常。

服务层(Service)

为业务逻辑层提供服务

职责

服务层,也作为防腐层,它用来封装对数据传输层的技术调用细节,为业务逻辑层提供具有业务含义的原子性业务动作业务数据

其中的方法命名要尽可能的拥有业务含义。例如关闭订单这个场景,其应该提供一个closeOrder(long orderId)方法,内部技术细节则可能是调用mybatis的Mapper将订单表的status字段由0更新为1.

异常情况:该层可能发生的异常有,你代码本身的bug导致的异常,以及调用数据传输层产生的系统异常。为了业务逻辑层可以感知到该层的异常,该层的异常通常不需要自己捕获,用默认抛给业务逻辑层即可。

业务逻辑层

该层就是用来实现业务流程。

该层就是用来调用服务层,实现业务流程的编排。该层作为业务逻辑层,要尽可能的保证业务逻辑的可读性强,而不被一些技术细节所干扰。

异常情况:该层抛出的异常,是在不满足某个业务前提条件时,终止业务流程的业务异常。以及调用服务层可能产生的系统异常。

接口层

作为服务对外的通道,要保证确定性的输入和确定性的输出。

职责

  • 入参校验

    对参数进行校验、解析、以及初始化工作。尽可能保证进入业务逻辑层前,参数是可靠符合预期的。

  • 结果返回

    业务逻辑执行完后,将结果响应给调用方。通常是将响应数据进行序列化以方便网络传输。

  • 异常处理

    这是比较重要的一个职责,也是最容易被忽略的。

    • 该层在对入参进行校验,可能会有参数异常产生,这时我们需要捕获该异常,将异常告知调用方。
    • 该层还会调用业务逻辑层完成业务流程,则可能会收到业务逻辑层抛出来的业务异常和系统异常。我们也需要将异常捕获,通过某个手段告知调用方。
  • 日志打印

    为了方便问题排查我们通常需要打印日志。在该层我们可以根据自身情况打印如下日志。

    • 入参
    • 异常
      • 异常信息,尤其是系统异常一定要打印,用于排查问题必不可少的依据。对于参数异常和业务异常,可无需打印或者以warn级别打印,因为这不是你的错。

异常情况:该层要对所有异常进行捕获处理。通常我们接口响应数据中定义code码,来告知调用方本次调用的异常情况。不要再往上抛异常,否则你就无法确定调用方到底得到怎样的响应。

以上就是分层的详细说明。还很抽象对吧,没关系,看个示例感受感受。

示例

数据传输层

由于该层不需要开发,所以就不再贴代码示例了。

服务层

订单服务

@Service
public class OrderService {
    @Resource
    private OrderMapper orderMapper;

    /**
     * 订单是否存在。
     * @param id
     * @return
     */
    public boolean isExist(long id) {
        //调用mapper查询数据库该订单是否存在
    }

    /**
     * 关闭订单
     * @param id
     */
    public void closeOrder(long id) {
        //调用mapper将状态字段从0更新为1
    }
}

消息队列服务

@Service
public class MqService {
    public void sendOrderClosedMsg(long orderId,String msg){
        //构建消息体
        //调用mq客户端发消息
    }
}
业务逻辑层

订单关闭的业务逻辑(该处业务流程纯虚构)

@Service
public class OrderBiz {
    @Resource
    private OrderService orderService;
    @Resource
    private MqService mqService;

    public void closeOrder(long id)throws Exception{
        //判断订单是否存在
        if(!orderService.isExist(id)){
            throw new BizException("订单不存在!");
        }
        //关闭订单
        orderService.closeOrder(id);
        //发送订单关闭消息
        mqService.sendOrderClosedMsg(id,"订单已关闭");
    }
}
接口层

接口层参数校验、异常处理、响应返回

@RestController
@RequestMapping("/order")
public class OrderController {
    private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);
    @Resource
    private OrderBiz orderBiz;

    @RequestMapping("/close")
    public Response<Void> closeOrder(Long orderId){
        try {
            if(orderId == null || orderId < 1){
                throw new ParamException("订单id不合法");
            }
            orderBiz.closeOrder(orderId);
            return Response.success(null);
        }catch (ParamException e){//参数异常
            return new Response<>(ResponseCode.PARAM_ERROR,e.getMessage());
        }catch (BizException e){//业务异常
            return new Response<>(ResponseCode.BIZ_EXCEPTION,e.getMessage());
        }catch (Exception e){//保证任何异常都能被捕获
            LOGGER.error("关闭订单时发生异常:",e);
            return new Response<>(ResponseCode.SERVER_ERROR,"服务端开小差,请稍后重试!");
        }
    }
}

响应实体

@Data
public class Response<T> {

    protected Integer code;
    protected String errMsg;
    protected T data;

    public Response() {
    }

    public Response(Integer code, String errMsg) {
        this.code = code;
        this.errMsg = errMsg;
    }

    public static <T> Response<T> success(T data){
        Response  response = new Response();
        response.setCode(ResponseCode.SUCCESS);
        response.setData(data);
        return response;
    }

}

以上就是本人关于分层架构的最佳实践。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容