学代码,看世界

概述

同事离职跑路,留下一大堆的坑等你填;简单需求,却需要大改代码;修改一处代码,却影响另一处代码;代码耦合度太高,牵一发而动全身;系统的维护成本越来越大。

作为一个开发人员,我相信大家或多或少都面临过这些比较普遍的困扰。但是受限于技术水平,面对这些困扰时往往一筹莫展。想要写出优秀的代码,好像唯有提升技术水平一途。然而,技术水平的提升绝非一日之功。因此,我最近一直在思考,试图为公司制定出一套基于面向对象思想的编码规范,只要遵守该规范,就可以相对写出稳定性好、可读性强,并且可扩展性高的代码,使得整个系统呈现出高内聚、低耦合的特性。

但是如何写出一款优秀的程序,这是一个比较大的话题,一篇文章很难面面俱到,所以,我打算用多篇文章从不同的角度来阐述。好了,废话不多说,本文的主题是围绕 封装单一 两个角度,提炼开发规范。

封装的概念

前面已经讲过,整篇文章都是围绕 封装单一 这两个核心展开分析的,因此,在正式分析之前,我们必须对这两个概念了如指掌。首先来讲 封装 的概念。

所谓 封装,官方给出的定义是把对象的属性和操作结合为一个独立的整体,并尽可能隐藏对象的内部实现细节。 而百度百科给出的解释是,封装是把过程和数据包围起来,对数据的访问只能通过已定义的接口。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。封装是一种信息隐藏技术。封装把对象的所有组成部分组合在一起,封装定义程序如何引用对象的数据,封装实际上使用方法将类的数据隐藏起来,控制用户对类的修改和访问数据的程度。 适当的封装可以让程序代码更容易理解和维护,也加强了程序代码的安全性。

上面对封装概念的解释,看似抽象,实则非常精炼,正如百度百科中所说,面相对象计算始于封装的概念,这句话足以说明封装在面相对象中的重要性了,说它是面相对象思想的灵魂也不为过。

单一的概念

首先说明,这里的单一,指的是 单一职责原则单一功能原则,说法不同,含义相同。

原则是指经过长期经验总结所得出的合理化的现象,是人类言行所依据的准则。在面向对象程序设计中,也同样存在原则,比如 单一职责原则,它是我们开发中的一个最基础的、必须遵守的原则性指导思想。

深入的描述待补充 。。。

项目代码

下面,我用实际工作中的项目代码作为示例,先分析代码中存在的问题,然后给出解决方案,最后再从解决方案中提炼出开发规范。

项目背景

我们公司开发了一套自己的支付系统,为内部其他系统提供基础的支付服务。系统集成了支付宝支付、微信支付、中通支付、汇潮支付等 10 家第三方支付公司。每家第三方支付公司提供 10 个支付方式,比如微信支付提供的二维码支付、H5支付、小程序支付、公众号支付等。这样算下来,支付系统一共对接了 100 个支付方式。

需求描述

用户发起支付时,支付系统创建订单。订单创建完成后五分钟之内未完成支付,则更新订单状态为 过期未支付,支付成功时,更新订单状态为 支付成功,支付失败时,更新订单状态为 支付失败

过期未支付对应的状态码为 3,支付失败对应的状态码为 2,支付成功对应的状态码为 1

代码实现

{.tabset}
订单服务

订单服务接口:

public interface OrderService {

    /**
     * 通用更新方法
     */
    boolean updateOrder(Order order);
}

订单服务实现类:

@Service("orderService")
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderDao orderDao;

    /**
     * 通用更新方法
     */
    public boolean updateOrder(Order order) {
        return orderDao.update(order);
    }
}
支付服务

支付系统中共有 100 个支付方式,每个支付方式对应着一个支付服务。这里,我们用微信公众号支付方式对应的支付服务(如无特别说明,后面简称支付服务)作为示例:

/**
 * 微信公众号支付服务
 */
@Slf4j
@Service
public class JsapiPayment {

    @Resource
    private OrderService orderService;

    /**
     * 微信支付结果回调通知,根据支付结果,调用订单服务更新订单状态
     */
    public void paymentResultCallback(String orderNo, String paymentResult) {

        // 处理微信公众号支付相关逻辑......

        Order order = new Order();
        order.setOrderNo(orderNo);
        if ("支付成功".equals(paymentResult)) {
            order.setOrderStatus("1");
        } else if ("支付失败".equals(paymentResult)) {
            order.setOrderStatus("2");
        }
        order.setUpdateTime(new Date());
        orderService.updateOrder(order);
    }
}

代码分析

认真读完代码后,大家的第一感觉,这样的代码是否有问题?如果单纯从当前功能实现的角度来看,代码简洁易读,逻辑清晰明了,好像完全没有问题。但是,通过个人感觉来衡量一个程序的好坏,这是远远不够的,我们需要从 稳定性可读性可扩展性 等多个角度综合判断。

如果不满足 单一功能原则 ,一个类或方法负责多个功能,必然会导致各个功能之间相互存在耦合,如果某个功能需要修改时,不得不考虑是否会对其他功能带来影响,甚至,修改一个功能,会直接导致其他功能无法使用。这就不符合低耦合的要求,程序的稳定性、可读性和可扩展性都很差。

封装是一种信息隐藏技术,如果 隐藏 做的不够好,把自己的数据对外暴露,那么外部就可以直接操作数据,外部的错误可能会影响内部逻辑,如果内部的数据结构发生变化,同样可能会影响到所有的外部调用方,且这样的影响面更大。这就不合符高内聚的要求,我自己的数据自己内部维护,绝不让外人指手画脚,或者说,自己的数据高度聚集在自己类的内部,这就是高内聚。

至此,该做的铺垫做了,该说的概念也说完了,现在就正式进入代码的分析阶段吧。

分析一

订单服务的本质就是为了维护订单数据,包括订单的创建,订单状态的变更,历史订单的查询,垃圾订单的删除等。所以,Order 实体属于订单服务的数据。但是,上面的代码中,支付服务依赖了 Order 实体,这样的代码,高内聚、低耦合 方面做的是非常差的。

如果某天来了,让我把支付成功的订单状态码修改为 PAY-SUCCEED,我们至少需要修改 100 个支付服务中的状态码,那如果订单新增或删除一个状态码呢,对支付服务的影响又有多大?要是在现实世界中,我想支付服务肯定要骂娘了。作为支付服务,我为什么要关心你订单中的破状态码?你订单服务把状态码封装到自己内部,你想怎么改就怎么改,对我们广大的支付服务没有任何影响。各行其道,互不影响,这样不香吗?

分析二

过了几天,某天又来了,让我把 Order 实体中的状态码字段的名称改掉,这样会出现什么结果?毫无疑问,广大的支付服务又要受伤了,上百个支付服务全部报错。还是那句话,作为支付服务,我为什么要关心你订单中某个数据的字段名?你订单服务把你的所有数据都封装到自己内部,你想怎么改就怎么改,对我们广大的支付服务没有任何影响。各行其道,互不影响,这样不香吗?

分析三

再过几天,某天再次来了,提出了新的要求,当订单支付成功时,给用户发送一封邮件,订单支付失败时,给用户发送一条短信。不出意外,上百个支付服务悲催了,再次躺枪,所有支付服务中,所有出现成功状态码的地方,均要调用邮件服务,所有出现失败状态码的地方,均要调用短信服务。站在邮件服务和短信服务角度,人家只需要和订单服务建立关联关系就行了,你非要让他们和上百个支付服务都要建立关联关系。有些小聪明可能会想到,在订单服务提供的通用更新方法 updateOrder(Order order) 中,使用 if...else... 判断订单状态,然后根据判断结果选择调用短信服务或邮件服务,这样一来,邮件服务和短信服务只和订单有关联关系,且只需要修改订单服务的通用更新方法即可。这样真的可行吗?我只能说,你太年轻了。原因是订单服务提供的通用更新方法 updateOrder(Order order) 违背了 单一职责原则 ,它的功能很强大,可以更新订单中的所有字段。假如一个已经处于支付失败状态的订单,调用方调用此方法希望把订单的付款金额改小一点,让穷酸用户也可以支付得起,此时,更新方法的操作仅仅是更新金额,你却通过判断订单状态为支付失败,再次给用户发送了一条支付失败的短信,这是不是就出BUG了?人家本来就有点穷,第一次收到支付失败短信就感觉挺尴尬的,你非要再发一条给人家,往人家伤口上撒盐。记住,在修改违背 单一职责原则 的方法时,一定要考虑到所有业务场景的方方面面,否则,一不小心就会捡到BUFF

分析四

随着公司发展越来越好,系统中各服务的数据量越来越大,传统应用已经无法满足业务需要。现在需要把系统中的订单服务单独抽离出来,作为一个独立的微服务。上面的代码又该如何应对这样的变化?大牛哥写的代码,可以轻松应对业务的千变万化,而上面的代码呢,如果业务经常变化,你的头发还能保住几根?

对于上面的代码,缺点说完了,我们再来聊聊优点。但是,好像实在是找不到什么优点,如果硬要找一个,可能是订单服务的代码量稍微少一点吧。对于喜欢看优点的人,请移步下面的优化方案。对比上方的四个分析来阅读下方的优化方案效果最好。

优化方案

{.tabset}
订单服务

订单服务接口:

public interface OrderService {

    /**
     * 订单支付成功
     */
    boolean orderPaySucceed(String orderNo);

    /**
     * 订单支付失败
     */
    boolean orderPayFail(String orderNo);
}

订单服务实现类:

@Service("orderService")
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderDao orderDao;

    /**
     * 订单支付成功
     */
    public boolean orderPaySucceed(String orderNo) {
        // 这里扩展邮件相关逻辑......

        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setOrderStatus("1");
        order.setUpdateTime(new Date());
        return orderDao.update(order);
    }

    /**
     * 订单支付失败
     */
    public boolean orderPayFail(String orderNo) {
        // 这里扩展短信相关逻辑......

        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setOrderStatus("2");
        order.setUpdateTime(new Date());
        return orderDao.update(order);
    }
}
支付服务

支付系统中共有 100 个支付方式,每个支付方式对应着一个支付服务。这里,我们用微信公众号支付方式对应的支付服务(如无特别说明,后面简称支付服务)作为示例:

/**
 * 微信公众号支付服务
 */
@Slf4j
@Service
public class JsapiPayment {

    @Resource
    private OrderService orderService;

    /**
     * 微信支付结果回调通知,根据支付结果,调用订单服务更新订单状态
     */
    public void paymentResultCallback(String orderNo, String paymentResult) {

        // 处理微信公众号支付相关逻辑......

        if ("支付成功".equals(paymentResult)) {
            orderService.orderPaySucceed("ORDER267652C4490AI03");
        } else if ("支付失败".equals(paymentResult)) {
            orderService.orderPayFail("ORDER267652C4490AI03");
        }
    }
}

规范总结

待补充。。。

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

推荐阅读更多精彩内容

  • 我对这个系统的了解 现在有如下几个角色: 平台:龙果支付系统, 商户:使用龙果支付系统的用户,比如某公司的商城系统...
    程序yuan阅读 1,130评论 0 2
  • 关于微信支付 1. 生活中的微信支付   目前我们日常生活中接触得比较多的线上电子支付方式主要有两种,一种是支付宝...
    小流歌_阅读 594评论 0 0
  • 基于PHP的微信支付教程 微信支付作为各大移动支付方式之一,本课程只要向大家介绍并使用微信支付的常用功能,进而集合...
    獨愛凌亂中那抹獨特的華麗阅读 8,550评论 0 7
  • 关于微信支付 生活中的微信支付 目前我们日常生活中接触得比较多的线上电子支付方式主要有两种,一种是支付宝,另一种就...
    积_渐阅读 3,915评论 3 26
  • 第1题 【说明】某医疗器械公司作为复杂医疗产品的集成商,必须保持高质量部件的及时供应。为了实现这一目标,该公司欲开...
    coderTG阅读 6,033评论 0 3