项目都做不好,还过啥程序员节?
1. 程序员的宿命?
孤立系统的一切自发过程均向着令其状态更无序的方向发展,如果要使系统恢复到原先的有序状态是不可能的,除非外界对它做功。
掀桌子另起炉灶派: 很多人把项目做烂的原因归咎于项目前期的基础没打好、需求不稳定一路打补丁、前面的架构师和程序员留下的烂摊子难以收拾。 他们要么没有信心去收拾烂摊子,要么觉得这是费力不讨好,于是要放弃掉项目,寄希望于出现一个机会能重头再来。 但是他们对于如何避免重蹈覆辙、做出另一个烂项目是没有把握也没有深入思考的,只是盲目乐观的认为自己比前任更高明。 激进改革派: 这个派别把原因归结于烂项目当初没有采用正确的编程语言、最新最强大的技术栈或工具。 他们中一部分人也想着有机会另起炉灶,用上时下最流行最热门的技术栈(spring boot、springcloud、redis、nosql、docker、vue)。 或者即便不另起炉灶,也认为现有技术栈太过时无法容忍了(其实可能并不算过时),不用微服务不用分布式就不能接受,于是激进的引入新技术栈,鲁莽的对项目做大手术。 这种对刚刚流行还不成熟技术的盲目跟风、技术选型不慎重的情况非常普遍,今天在他们眼中落伍的技术栈,其实也不过是几年前另一批人赶的时髦。 我不反对技术上的追新,但是同样的,这里的问题是:他们对于大手术的风险和副作用,对如何避免重蹈覆辙用新技术架构做出另一个烂项目,没有把握也没有深入思考的,只是盲目乐观的认为新技术能带来成功。 也没人能阻止这种简历驱动的技术选型浮躁风气,毕竟花的是公司的资源,用新东西显得自己很有追求,失败了也不影响简历美化,简历上只会增加一段项目履历和几种精通技能,不会提到又做烂了一个项目,名利双收稳赚不赔。 保守改良派: 还有一类人他们不愿轻易放弃这个有问题但仍在创造效益的项目,因为他们看到了项目仍然有维护的价值,也看到了另起炉灶的难度(万事开头难,其实项目的冷启动存在很多外部制约因素)、大手术对业务造成影响的代价、系统迁移的难度和风险。 同时他们尝试用温和渐进的方式逐步改善项目质量,采用一系列工程实践(主要包括重构热点代码、补自动化测试、补文档)来清理“技术债”,消除制约项目开发效率和交付质量的瓶颈。
2. 一个 35+ 程序员的反思
从技术选型到架构设计到代码规范,都是我自己做的,团队不大,也是我自己组建和一手带出来的; 最开始的半年进展非常顺利,用着我最趁手的技术和工具一路狂奔,年底前替换掉了之前采购的那个垃圾产品(对的,有个前任在业务上做参照也算是个很大的有利因素); 做的过程我也算是全力以赴,用尽毕生所学——前面 13 年工作的经验值和走过的弯路、教训,使得公司只用其它同类公司同类项目 20% 的资源就把平台做起来了; 如果说多快好省是最高境界,那么当时的我算是做到了多、快、省——交付的功能非常丰富且贴近业务需求、开发节奏快速、对公司开发资源很节省; 但是现在看来,“好”就远远没有达到了,到了项目中期,简单优先级高的需求都已经做完了,公司业务上出现了新的挑战——接入另一个核心系统以及外部平台,真正的考验来了。 那个改造工程影响面比较大,需要对我们的系统做大面积修改,最麻烦的是这意味着从一个简单的单体系统变成了一个分布式的系统,而且业务涉及资金交易,可靠性要求较高,是难上加难。 于是问题开始出现了:我之前架构的优点——简单直接——这个时候不再是优点了,简单直接的架构在业务环境、技术环境都简单的情况下可以做到多快好省,但是当业务、技术环境都陡然复杂起来时,就不行了; 具体的表现就是:架构和代码层面的结构都快速的变得复杂、混乱起来了——熵急剧增加; 后面的事情就一发不可收拾:代码改起来越来越吃力、测试问题变多、生产环境故障和问题变多、于是消耗在排查测试问题生产问题和修复数据方面的精力急剧增加、出现恶性循环。。。 到了这个境地,项目就算是做烂了!一个我从头开始做起的没有任何借口的失败!
质量要素不是一个可以被牺牲和妥协的要素——牺牲质量会导致其它三要素全都受损,反之同理,追求质量会让你在其它三个方面同时受益。 在保持一个质量水平的前提下,成本、进度、范围三要素确确实实是互相制约关系——典型的比如牺牲成本(加班加点)来加快进度交付急需的功能。 正如著名的“破窗效应”所启示的那样:任何一种不良现象的存在,都在传递着一种信息,这种信息会导致不良现象的无限扩展,同时必须高度警觉那些看起来是偶然的、个别的、轻微的“过错”,如果对这种行为不闻不问、熟视无睹、反应迟钝或纠正不力,就会纵容更多的人“去打烂更多的窗户玻璃”,就极有可能演变成“千里之堤,溃于蚁穴”的恶果——质量不佳的代码之于一个项目,正如一扇破了的窗之于一幢建筑、一个蚂蚁巢之于一座大堤。 好消息是,只要把质量提上去项目就会逐渐走上健康的轨道,其它三个方面也都会改善。管好了质量,你就很大程度上把握住了项目成败的关键因素。 坏消息是,项目的质量很容易失控,现实中质量不佳、越做越臃肿混乱的项目比比皆是,质量改善越做越好的案例闻所未闻,以至于人们将其视为如同物理学中“熵增加定律”一样的必然规律了。 当然任何事情都有一个度的问题,当质量低于某个水平时才会导致其它三要素同时受损。反之当质量高到某个水平以后,继续追求质量不仅得不到明显收益,而且也会损害其它三要素——边际效用递减定律。 这个度需要你为自己去评估和测量,如果目前的质量水平还在两者之间,那么就应该重点改进项目质量。当然,现实世界中很少看到哪个项目质量高到了不需要重视的程度。
3. 项目走向衰败的最常见诱因——代码质量不佳
4. 一个失败项目复盘
这是该项目中一个最核心、最复杂也是最经常要被改动的 class,代码行数 4881; 结果就是冗长的 API 列表(列表需要滚动 4 屏才能到底,公有私有 API 180 个);
还是那个 Class,头部的 import 延绵到了 139 行,去掉第一行 package 声明和少量空行总共 import 引入了 130 个 class!
还是那个坑爹的组件,从 156 行开始到 235 行声明了 Spring 依赖注入的组件 40 个!
4.1 症结 1:组件粒度过大、API 泛滥
业界关于如何设计业务逻辑层 并没有标准和最佳实践,绝大多数项目(我自己经历过的项目以及我有机会深入了解的项目)中大家都是想当然的按照业务领域对象来设计; 例如:领域实体对象有 Account、Order、Delivery、Campaign。于是业务逻辑层就设计出 AccountService、OrderService、DeliveryService、CampaignService 这种做法在项目简单是没什么问题,事实上项目简单时 你随便怎么设计都问题不大。 但是当项目变大和复杂以后,就会出现问题了: 组件臃肿:Service 组件的个数跟领域实体对象个数基本相当,必然造成个别 Service 组件变得非常臃肿——API 非常多,代码行数达到几千行; 职责模糊:业务逻辑往往跨多个领域实体,无论放在哪个 Service 都不合适,同样的,要找一个功能的实现逻辑也无法确定在哪个 Service 中; 代码重复 or 逻辑纠缠的两难选择:当遇到一个业务逻辑,其中的某个环节在另一个业务逻辑 API 中已经实现,这时如果不想忍受重复实现和代码,就只能去调用那个 API。但这样就造成了业务逻辑组件之间的耦合与依赖,这种耦合与依赖很快会扩散——新的 API 又会被其它业务逻辑依赖,最终形成蜘蛛网一样的复杂依赖甚至循环依赖; 复用代码、减少重复虽然是好的,但是复杂耦合依赖的害处也很大——赶走一只狼引来了一只虎。两杯毒酒给你选!
4.2 药方 1:倒金字塔结构——业务逻辑组件职责单一、禁止层内依赖
业务逻辑层应该被设计成一个个功能非常单一的小组件,所谓小是指 API 数量少、代码行数少; 由于职责单一因此必然组件数量多,每一个组件对应一个很具体的业务功能点(或者几个相近的); 复用(调用、依赖)只应该发生在相邻的两层之间——上层调用下层的 API 来实现对下层功能的复用; 于是系统架构就自然呈现出倒立的金字塔形状:越接近顶层的业务场景组件数量越多,越往下层的复用性高,于是组件数量越少。
4.3 症结 2:低内聚、高耦合
高内聚:组件本身应该尽可能的包含其所实现功能的所有重要信息和细节,以便让维护者无需跳转到其它多个地方去了解必要的知识。 低耦合:组件之间的互相依赖和了解尽可能少,以便在一个组件需要改动时其它组件不受影响。
业界关于“复用性”的认识存在一个误区——认为包括业务逻辑组件在内的任何层面的组件都应该追求最大限度的可复用性; 复用当然是好的,但那应该有个前提条件:不增加系统复杂度的情况下的复用,才是好的。 什么样的复用会增加系统复杂性、是不好的呢?前面提到的,一个业务逻辑 API 被另一个业务逻辑 API 复用——就是不好的: 损害了稳定性:因为业务逻辑本身是跟现实世界的业务挂钩的,而业务会发生变化;当你复用一个会发生变化的 API,相当于在沙子上建高楼——地基是松动的; 增加了复杂性:这样的依赖还造成代码可读性降低——在一个本就复杂的业务逻辑代码中,包含了对另一个复杂业务逻辑的调用,复杂度会急剧增加,而且会不断泛滥和传递; 内聚性被破坏:由于业务逻辑被打散在了多个组件的方法内,变得支离破碎,无法在一个地方看清整体逻辑脉络和实现步骤——内聚性被破坏,同时也意味着,这个调用链条上涉及的所有组件之间存在高耦合。
4.4 药方 2:复用的两种正确姿势——打造自己的 lib 和 framework
lib 库是供你(应用程序)调用的,它帮你实现特定的能力(比如日志、数据库驱动、json 序列化、日期计算、http 请求)。 framework 框架是供你扩展的,它本身就是半个应用程序,定义好了组件划分和交互机制,你需要按照其规则扩展出特定的实现并绑定集成到其中,来完成一个应用程序。 lib 就是组合方式的复用,framework 则是继承式的复用,继承的 Java 关键字是 extends,所以本质上是扩展。 过去有个说法:“组合优于继承,能用组合解决的问题尽量不要继承”。我不同意这个说法,这容易误导初学者以为组合优于继承,其实继承才是面向对象最强大的地方,当然任何东西都不能乱用。 典型的继承乱用就是为了获得父类的某个 API 而去继承,继承一定是为了扩展,而不是为了直接获得一个能力,获得能力应该调用 lib,父类不应该去实现具体功能,那是 lib 该做的事。 也不应该为了使用 lib 而去继承 lib 中的 Class。lib 就是用来被组合被调用的,framework 就是用来被继承、扩展的。 再展开一下:lib 既可以是第三方的(log4j、httpclient、fastjson),也可是你自己工程的(比如你的持久层 Dao、你的 utils); framework 同理,既可以是第三方的(springmvc、jpa、springsecurity),也可以是你项目内封装的面向具体业务领域的(比如 report、excel 导出、paging 或任何可复用的算法、流程)。 从这个意义上说,一个项目中的代码其实只有 3 种:自定义的 lib class、自定义的 framework 相关 class、扩展第三方或自定义 framework 的组件 class。 再扩展一下:相对于过去,现在我们已经有了足够多的第三方 lib 和 framework 来复用,来帮助项目节省大量代码,开发工作似乎变成了索然无味、没技术含量的 CRUD。但是对于业务非常复杂的项目,则需要有经验、有抽象思维、懂设计模式的人,去设计面向业务的 framework 和面向业务的 lib,只有这样才能交付可维护、可扩展、可复用的软件架构——高质量架构,帮助项目或产品取得成功。
4.5 症结 3:抽象不够、逻辑纠缠——High Level 业务逻辑和 Low Level 实现逻辑纠缠
输入合法性校验; 业务规则校验:典型的如检查交易记录状态、金额、时限、权限等,通常包含数据库或外部接口的查询作为参考; 数据持久化行为:数据库、缓存、文件、日志等任何形式的数据写入行为; 外部接口调用行为; 输出/返回值准备。
可读性变差:两个维度的复杂性——业务复杂性和底层实现的技术复杂性——被掺杂在了一起,复杂度 1+1>2 剧增,给其他人阅读代码增加很大负担; 可维护性差:可维护性通常指排查和解决问题所需花费的代价高低,当两个 level 的逻辑纠缠在一起,会使排查问题变的更困难,修复问题时也更容易出错; 可扩展性无从谈起:扩展性通常指为系统增加一个特性所需花费的代价高低,代价越高扩展性越差;与排查修复问题类似,逻辑纠缠显然也会使添加新特性变得困难、一不小心就破坏了已有功能。
@Override
public void updateFromMQ(String compress) {
try {
JSONObject object = JSON.parseObject(compress);
if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){
throw new AppException("MQ返回参数异常");
}
logger.info(object.getString("mobile")+"<<<<<<<<<获取来自MQ的授权数据>>>>>>>>>"+object.getString("type"));
Map map = new HashMap();
map.put("type",CrawlingTaskType.get(object.getInteger("type")));
map.put("mobile", object.getString("mobile"));
List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);
redisClientTemplate.set(object.getString("mobile") + "_" + object.getString("type"),CompressUtil.compress( object.getString("data")));
redisClientTemplate.expire(object.getString("mobile") + "_" + object.getString("type"), 2*24*60*60);
//保存成功 存入redis 保存48小时
CrawlingTask crawlingTask = null;
// providType:(0:新颜,1XX支付宝,2:ZZ淘宝,3:TT淘宝)
if (CollectionUtils.isNotEmpty(list)){
crawlingTask = list.get(0);
crawlingTask.setJsonStr(object.getString("data"));
}else{
//新增
crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), object.getString("data"),
object.getString("mobile"), CrawlingTaskType.get(object.getInteger("type")));
crawlingTask.setNeedUpdate(true);
}
baseDAO.saveOrUpdate(crawlingTask);
//保存芝麻分到xyz
if ("3".equals(object.getString("type"))){
String data = object.getString("data");
Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
Map param = new HashMap();
param.put("phoneNumber", object.getString("mobile"));
List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
if (list1 !=null){
for (Dperson dperson:list1){
dperson.setZmScore(zmf);
personBaseDaoI.saveOrUpdate(dperson);
AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);//查询多租户表 身份认证、淘宝认证 为0 置为1
}
}
}
} catch (Exception e) {
logger.error("更新my MQ授权信息失败", e);
throw new AppException(e.getMessage(),e);
}
}
4.6 药方 3:控制逻辑分离——业务模板 Pattern of NestedBusinessTemplate
根据经验,当我们着手维护一段代码时,一定是想先弄清楚它的整体流程、算法和行为,而不是一上来就去研究它的细枝末节; 控制逻辑分离后,只需要去看 High Level 部分就能了解到上述内容,阅读代码的负担大幅度降低,代码可读性显著增强; 读懂代码是后续一切维护、重构工作的前提,而且一份代码被读的次数远远高于被修改的次数(高一个数量级),因此代码对人的可读性再怎么强调都不为过,可读性增强可以大幅度提高系统可维护性,也是重构的最主要目标。 同时,根据我的经验,High Level 业务逻辑的变更往往比 Low Level 实现逻辑变更要来的频繁,毕竟前者跟业务直接对应。当然不同类型项目情况不一样,另外它们发生变更的时间点往往也不同; 在这样的背景下,控制逻辑分离的好处就更明显了:每次维护、扩充系统功能只需改动一个 Levle 的代码,另一个 Level 不受影响或影响很小,这会大幅降低修改成本和风险。
public class XyzService {
abstract class AbsUpdateFromMQ {
public final void doProcess(String jsonStr) {
try {
JSONObject json = doParseAndValidate(jsonStr);
cache2Redis(json);
saveJsonStr2CrawingTask(json);
updateZmScore4Dperson(json);
} catch (Exception e) {
logger.error("更新my MQ授权信息失败", e);
throw new AppException(e.getMessage(), e);
}
}
protected abstract void updateZmScore4Dperson(JSONObject json);
protected abstract void saveJsonStr2CrawingTask(JSONObject json);
protected abstract void cache2Redis(JSONObject json);
protected abstract JSONObject doParseAndValidate(String json) throws AppException;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public void processAuthResultDataCallback(String compress) {
new AbsUpdateFromMQ() {
@Override
protected void updateZmScore4Dperson(JSONObject json) {
//保存芝麻分到xyz
if ("3".equals(json.getString("type"))){
String data = json.getString("data");
Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
Map param = new HashMap();
param.put("phoneNumber", json.getString("mobile"));
List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
if (list1 !=null){
for (Dperson dperson:list1){
dperson.setZmScore(zmf);
personBaseDaoI.saveOrUpdate(dperson);
AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);
}
}
}
}
@Override
protected void saveJsonStr2CrawingTask(JSONObject json) {
Map map = new HashMap();
map.put("type",CrawlingTaskType.get(json.getInteger("type")));
map.put("mobile", json.getString("mobile"));
List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);
CrawlingTask crawlingTask = null;
// providType:(0:xx,1yy支付宝,2:zz淘宝,3:tt淘宝)
if (CollectionUtils.isNotEmpty(list)){
crawlingTask = list.get(0);
crawlingTask.setJsonStr(json.getString("data"));
}else{
//新增
crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), json.getString("data"),
json.getString("mobile"), CrawlingTaskType.get(json.getInteger("type")));
crawlingTask.setNeedUpdate(true);
}
baseDAO.saveOrUpdate(crawlingTask);
}
@Override
protected void cache2Redis(JSONObject json) {
redisClientTemplate.set(json.getString("mobile") + "_" + json.getString("type"),CompressUtil.compress( json.getString("data")));
redisClientTemplate.expire(json.getString("mobile") + "_" + json.getString("type"), 2*24*60*60);
}
@Override
protected JSONObject doParseAndValidate(String json) throws AppException {
JSONObject object = JSON.parseObject(json);
if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))){
throw new AppException("MQ返回参数异常");
}
logger.info(object.getString("mobile")+"<<<<<<<<<获取来自MQ的授权数据>>>>>>>>>"+object.getString("type"));
return object;
}
}.doProcess(compress);
}
把 Low Level 逻辑提取成 private function,被 High Level 代码所在的 function 直接调用; 问题 1 硬连接不灵活:首先,这样虽然起到了一定的隔离效果,但是两个 level 之间是静态的硬关联,Low Level 无法被简单的替换,替换时还是需要修改和影响到 High Level 部分; 问题 2 组件内可见性造成混乱:提取出来的 private function 在当前组件内是全局可见的——对其它无关的 High Level function 也是可见的,各个模块之间仍然存在逻辑纠缠。这在很多项目中的热点代码中很常见,问题也很突出:试想一个包含几十个 API 的组件,每个 API 的 function 存在一两个关联的 private function,那这个组件内部的混乱程度、维护难度是难以承受的。 把 Low Level 逻辑抽取到新的组件中,供 High Level 代码所在的组件依赖和调用;更有经验的程序员可能会增加一层接口并且借助 Spring 依赖注入; 问题 1 API 泛滥:提取出新的组件似乎避免了“结构化编程”的局限性,但是带来了新的问题——API 泛滥:因为组件之间调用只能走 public 方法,而这个 API 其实没有太多复用机会根本没必要做成 public 这种最高可见性。 问题 2 同层组件依赖失控:组件和 API 泛滥后必然导致组件之间互相依赖成为常态,慢慢变得失控以后最终变成所有组件都依赖其它大部分组件,甚至出现循环依赖;比如那个拥有 130 个 import 和 40 个 Spring 依赖组件的 ContractService。
High Level逻辑封装在抽象父类AbsUpdateFromMQ的一个final function中,形成一个业务逻辑的模板; final function保证了其中逻辑不会被子类有意或无意的篡改破坏,因此其中封装的一定是业务逻辑中那些相对固定不变的东西。至于那些可变的部分以及暂时不确定的部分,以abstract protected function形式预留扩展点; 子类(一个匿名内部类)像“做填空题”一样,填充模板实现Low Level逻辑——实现那些protected function扩展点;由于扩展点在父类中是abstract的,因此编译器会提醒子类的程序员该扩展什么。
Low Level 需要修改或替换时,只需从父类扩展出一个新的子类,父类全然不知无需任何改动; 无论是父类还是子类,其中的 function 对外层的 XyzService 组件都是不可见的,即便是父类中的 public function 也不可见,因为只有持有类的实例对象才能访问到其中的 function; 无论是父类还是子类,它们都是作为 XyzService 的内部类存在的,不会增加新的 java 类文件更不会增加大量无意义的 API(API 只有在被项目内复用或发布出去供外部使用才有意义,只有唯一的调用者的 API 是没有必要的); 组件依赖失控的问题当然也就不存在了。
4.7 症结 4:无处不在的 if else 牛皮癣
if else if ...else 以及类似的 switch 控制语句,本质上是一种 hard coding 硬编码行为,如果你同意“magic number 魔法数字”是一种错误的编程习惯,那么同理,if else 也是错误的 hard coding 编程风格; hard coding 的问题在于当需求发生改变时,需要到处去修改,很容易遗漏和出错; 以一段代码为例来具体分析:
if ("3".equals(object.getString("type"))){
String data = object.getString("data");
Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
Map param = new HashMap();
param.put("phoneNumber", object.getString("mobile"));
List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
if (list1 !=null){
for (Dperson dperson:list1){
dperson.setZmScore(zmf);
personBaseDaoI.saveOrUpdate(dperson);
AppFlowUtil.updateAppUserInfo(dperson.getToken(),null,null,zmf);
}
}
}
if ("3".equals(object.getString("type"))) 显然这里的"3"是一个 magic number,没人知道 3 是什么含义,只能推测; 但是仅仅将“3”重构成常量 ABC_XYZ 并不会改善多少,因为 if (ABC_XYZ.equals(object.getString("type"))) 仍然是面向过程的编程风格,无法扩展; 到处被引用的常量 ABC_XYZ 并没有比到处被 hard coding 的 magic number 好多少,只不过有了含义而已; 把常量升级成 Enum 枚举类型呢,也没有好多少,当需要判断的类型增加了或判断的规则改变了,还是需要到处修改——Shotgun Surgery(霰弹式修改) 并非所有的 if else 都有害,比如上面示例中的 if (list1 !=null) { 就是无害的,没有必要去消除,也没有消除它的可行性。判断是否有害的依据: 如果 if 判断的变量状态只有两种可能性(比如 boolean、比如 null 判断)时,是无伤大雅的; 反之,如果 if 判断的变量存在多种状态,而且将来可能会增加新的状态,那么这就是个问题; switch 判断语句无疑是有害的,因为使用 switch 的地方往往存在很多种状态。
4.8 药方 4:充血枚举类型——Rich Enum Type
实现多种系统通知机制,传统做法:
enum NOTIFY_TYPE { email,sms,wechat; } //先定义一个enum——一个只定义了值不包含任何行为的“贫血”的枚举类型
if(type==NOTIFY_TYPE.email){ //if判断类型 调用不同通知机制的实现
。。。
}else if (type=NOTIFY_TYPE.sms){
。。。
}else{
。。。
}
实现多种系统通知方式,充血枚举类型——Rich Enum Type 模式:
enum NOTIFY_TYPE { //1、定义一个包含通知实现机制的“充血”的枚举类型
email("邮件",NotifyMechanismInterface.byEmail()),
sms("短信",NotifyMechanismInterface.bySms()),
wechat("微信",NotifyMechanismInterface.byWechat());
String memo;
NotifyMechanismInterface notifyMechanism;
private NOTIFY_TYPE(String memo,NotifyMechanismInterface notifyMechanism){//2、私有构造函数,用于初始化枚举值
this.memo=memo;
this.notifyMechanism=notifyMechanism;
}
//getters ...
}
public interface NotifyMechanismInterface{ //3、定义通知机制的接口或抽象父类
public boolean doNotify(String msg);
public static NotifyMechanismInterface byEmail(){//3.1 返回一个定义了邮件通知机制的策的实现——一个匿名内部类实例
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){
.......
}
};
}
public static NotifyMechanismInterface bySms(){//3.2 定义短信通知机制的实现策略
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){
.......
}
};
}
public static NotifyMechanismInterface byWechat(){//3.3 定义微信通知机制的实现策略
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){
.......
}
};
}
}
//4、使用场景
NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
充血枚举类型——Rich Enum Type 模式的优势: 不难发现,这其实就是 enum 枚举类型和 Strategy Pattern 策略模式的巧妙结合运用; 当需要增加新的通知方式时,只需在枚举类 NOTIFY_TYPE 增加一个值,同时在策略接口 NotifyMechanismInterface 中增加一个 by 方法返回对应的策略实现; 当需要修改某个通知机制的实现细节,只需修改 NotifyMechanismInterface 中对应的策略实现; 无论新增还是修改通知机制,调用方完全不受影响,仍然是 NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg); 与传统 Strategy Pattern 策略模式的比较优势:常见的策略模式也能消灭 if else 判断,但是实现起来比较麻烦,需要开发更多的 class 和代码量: 每个策略实现需单独定义成一个 class; 还需要一个 Context 类来做初始化——用 Map 把类型与对应的策略实现做映射; 使用时从 Context 获取具体的策略; Rich Enum Type 的进一步的充血: 上面的例子中的枚举类型包含了行为,因此已经算作充血模型了,但是还可以为其进一步充血; 例如有些场景下,只是要对枚举值做个简单的计算获得某种 flag 标记,那就没必要把计算逻辑抽象成 NotifyMechanismInterface 那样的接口,杀鸡用了牛刀; 这时就可以在枚举类型中增加 static function 封装简单的计算逻辑; 策略实现的进一步抽象: 当各个策略实现(byEmail bySms byWechat)存在共性部分、重复逻辑时,可以将其抽取成一个抽象父类; 然后就像前一章节——业务模板 Pattern of NestedBusinessTemplate 那样,在各个子类之间实现优雅的逻辑分离和复用。
5. 重构前的火力侦察:为你的项目编制一套代码库目录/索引——CODEX
职责单一、小颗粒度、高内聚、低耦合的业务逻辑层组件——倒金字塔结构; 打造项目自身的 lib 层和 framework——正确的复用姿势; 业务模板 Pattern of NestedBusinessTemplate——控制逻辑分离; 充血的枚举类型 Rich Enum Type——消灭硬编码风格的 if else 条件判断;
在阅读代码过程中,在关键位置添加结构化的注释,形如://CODEX ProjectA 1 体检预约流程 1 预约服务 API 入口
所谓结构化注释,就是在注释内容中通过规范命名的编号前缀、分隔符等来体现出其所对应的项目、模块、流程步骤等信息,类似文本编辑中的标题 1、2、3; 然后设置 IDE 工具识别这种特殊的注释,以便结构化的显示。Eclipse 的 Tasks 显示效果类似下图;
这个结构化视图,本质上相对于是代码库的索引、目录,不同于 javadoc 文档,CODEX 具有更清晰的逻辑层次和更强的代码查找便利性,在 Eclipse Tasks 中点击就能跳转到对应的代码行; 这些结构化注释随着代码一起提交后就实现了团队共享; 这样的一份精确无误、共享的、活的源代码索引,无疑会对整个团队的开发维护工作产生巨大助力; 进一步的,如果在 CODEX 中添加 Markdown 关键字,甚至可以将导出的 CODEX 简单加工后,变成一张业务逻辑的 Sequence 序列图,如下所示。
6. 总结陈词——不要辜负这个程序员最好的时代
评论