代码精进之路 从码农到工匠.pdf
“Talk is cheap, show me the code”
代码即文档,可读性好的代码应该有一定的自明性,也就是不借助注释和文档,代码本身就能显性化地表达开发者的意图。
CRUD 操作 | 方法名约定 |
---|---|
新增 | create |
添加 | add |
删除 | remove |
修改 | update |
查询(单个结果) | get |
查询(多个结果) | list |
分页查询 | page |
统计 | count |
我们要通过 Regex 来获得字符串中的值,并放到 map 中。
Matcher matcher = headerPattern.matcher(line);
if (matcher.find()) {
headers.put(matcher.group(1), matcher.group(2));
}
用中间变量,可以写成如下形式:
Matcher matcher = headerPattern.matcher(line);
if (matcher.find()) {
String key = matcher.group(1);
String value = matcher.group(2);
headers.put(key, value);
}
在阿里巴巴有一个超级位置模型(Super Position Model,SPM)的埋点规范,用于统计分析各种场景的用户行为数据。 比如,淘宝社区电商业务(xTao)为外部合作伙伴(外站)提供的一套跟踪引导成交效果数据的解决方案,其中就用到了 SPM。
例如,一个跟踪点击到宝贝详情页的引导成交效果数据的 SPM 示例,其导购链接为 http://detail.tmall.com/item.htm?id=3716461318&spm=2014.123456789.1.2。
3456789.1.2 叫作 SPM 编码,是用于跟踪页面模块位置的编码,标准 SPM 编码由 4 段组成,采用 a.b.c.d 的格式。
通过基于这套规范采集的数据,我们可以利用 SPM 编码的不同层次来做不同维度的导购效果跟踪分析。
if (user != null) {
Address address = user.getAddress();
if (address != null) {
Country country = address.getCountry();
if (country != null) {
String isocode = country.getIsocode();
if (isocode != null) {
isocode = isocode.toUpperCase();
}
}
}
}
String isocode = Optional.ofNullable(user)
.flatMap(User::getAddress)
.flatMap(Address::getCountry)
.map(Country::getIsocode)
.orElse("default");
可以看到,新的写法比旧的判空方式在复杂度和简洁性上都提升了很多,简洁也是一种美。
抽象层次一致性(Single Level of Abstration Principle,SLAP),是和组合函数密切相关的一个原则。 组合函数要求将一个大函数拆成多个子函数的组合,而 SLAP 要求函数体中的内容必须在同一个抽象层次上。 如果高层次抽象和底层细节杂糅在一起,就会显得凌乱,难以理解。
举个例子,假如有一个冲泡咖啡的原始需求,其制作咖啡的过程分为 3 步。
其伪代码(pseudo code)如下:
public void makeCoffee() {
pourCoffeePowder();
pourWater();
stir();
}
如果要加入新的需求,比如需要允许选择不同的咖啡粉,以及选择不同的风味,那么代码就会变成这样:
public void makeCoffee(boolean isMilkCoffee, boolean isSweetTooth, CoffeeType type) {
// 选择咖啡粉
if (type == CAPPUCCINO) {
pourCappuccinoPowder();
} else if (type == BLACK) {
pourBlackPowder();
} else if (type == MOCHA) {
pourMochaPowder();
} else if (type == LATTE) {
pourLattePowder();
} else if (type == ESPRESSO) {
pourEspressoPowder();
}
// 加入沸水
pourWater();
// 选择口味
if (isMilkCoffee) {
pourMilk();
}
if (isSweetTooth) {
addSugar();
}
// 搅拌
stir();
}
如果继续有更多的需求加入,那么代码会进一步恶化,最后变成一个谁也看不懂且难以维护的逻辑迷宫。
再回看上面的代码,新需求的引入当然是根本原因。但除此之外,另一个原因是新代码已经不再满足 SLAP 了。具体选择用什么样的咖啡粉是倒入咖啡粉这个步骤应该去考虑的实现细节,和主流程步骤不在一个抽象层次上。同理,加奶和加糖也是实现细节。
因此,在引入新需求以后,制作咖啡的主要步骤从原来的 3 步变成了 4 步。
按照组合函数和 SLAP 原则,我们要在入口函数中只显示业务处理的主要步骤。具体的实现细节通过私有方法进行封装,并通过抽象层次一致性来保证,一个函数中的抽象在同一个水平上,而不是高层抽象和实现细节混杂在一起。
根据 SLAP 原则,我们可以将代码重构为:
public void makeCoffee(boolean isMilkCoffee, boolean isSweetTooth, CoffeeType type) {
// 选择咖啡粉
pourCoffeePowder(type);
// 加入沸水
pourWater();
// 选择口味
flavor(isMilkCoffee, isSweetTooth);
// 搅拌 stir();
}
private void flavor(boolean isMilkCoffee, boolean isSweetTooth) {
if (isMilkCoffee) {
pourMilk();
}
if (isSweetTooth) {
addSugar();
}
}
private void pourCoffeePowder(CoffeeType type) {
if (type == CAPPUCCINO) {
pourCappuccinoPowder();
} else if (type == BLACK) {
pourBlackPowder();
} else if (type == MOCHA) {
pourMochaPowder();
} else if (type == LATTE) {
pourLattePowder();
} else if (type == ESPRESSO) {
pourEspressoPowder();
}
}
重构后的 makeCoffee() 又重新变得整洁如初了,满足 SLAP 实际上是构筑了代码结构的金字塔。 金字塔结构是一种自上而下的,符合人类思维逻辑的表达方式。
在构筑金字塔的过程中,要求金字塔的每一层要属于同一个逻辑范畴、同一个抽象层次。在这一点上,金字塔原理和 SLAP 是相通的,世界就是如此奇妙,很多道理在不同的领域同样适用。
上面列举了 Spring 源码中的一个“坏味道”,接下来我们来看 Spring 的“好味道”。在 Spring 中,做上下文初始化的核心类 AbstractApplicationContext 的 refresh() 函数为我们在遵循 SLAP 方面做了一个很好的示范。
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining(non-lazy-init)singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
} catch (BeansException ex) {
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
} finally {
// Reset common introspection caches in Spring's core,
// since we might not ever need metadata for singleton
// beans anymore...
resetCommonCaches();
}
}
}
试想,如果上面的代码逻辑不是这样写,而是平铺在 refresh() 函数中,结果会是怎样?
SOLID 是 5 个设计原则开头字母的缩写,其本身就有“稳定的”的意思,寓意是“遵从SOLID原则可以建立稳定、灵活、健壮的系统”。5 个原则分别如下。
开闭原则和里氏代换原则是设计目标;单一职责原则、接口分隔原则和依赖倒置原则是设计方法。
以上提到了 GoF23 种设计模式的分类,简要介绍如下。
在拦截器模式中,主要包含以下角色。
public interface Target {
public Response execute(Request request);
}
public interface Interceptor {
public Response intercept(TargetInvocation targetInvocation);
}
public class TargetInvocation {
private List<Interceptor> interceptorList = new ArrayList<>();
private Iterator<Interceptor> interceptors;
private Target target;
private Request request;
public Response invoke() {
if ( interceptors.hasNext() ) {
Interceptor interceptor = interceptors.next();
// 此处是整个算法的关键,这里会递归调用 invoke()
interceptor.intercept(this); // 2
}
return target.execute(request);
}
public void addInterceptor(Interceptor interceptor) {
// 添加新的 Interceptor 到 TargetInvocation 中
interceptorList.add(interceptor);
interceptors = interceptorList.iterator();
}
}
public class AuditInterceptor implements Interceptor {
@Override
public Response intercept(TargetInvocation targetInvocation) {
if (targetInvocation.getTarget() == null) {
throw new IllegalArgumentException("Target is null");
}
System.out.println("Audit Succeeded ");
return targetInvocation.invoke();
}
}
LogInterceptor 实现如下:
public class LogInterceptor implements Interceptor {
@Override
public Response intercept(TargetInvocation targetInvocation) {
System.out.println("Logging Begin");
Response response = targetInvocation.invoke();
System.out.println("Logging End");
return response;
}
}
public class InterceptorDemo {
public static void main(String[] args) {
TargetInvocation targetInvocation = new TargetInvocation();
targetInvocation.addInterceptor(new LogInterceptor());
targetInvocation.addInterceptor(new AuditInterceptor());
targetInvocation.setRequest(new Request());
targetInvocation.setTarget(request->{return new Response();});
targetInvocation.invoke();
}
}
Logging Begin
Audit Succeeded
Logging End
在一个插件框架中,通常会涉及以下概念。
推荐一个开源项目 JPF(Java Plug-inFramework),它受到了 Eclipse 的插件式启发,致力于打造一个通用的 Java 插件框架。
精进就是你每天必须进步一点点!记住,慢就是快。千万不要忽视每天进步一点点的力量,也不要试图一口吃成胖子,真正的进步是滴水穿石的累积,这就是精进。
一个技术团队,不管大小,如果没有“技术味道”,那么技术 Leader 负有很大的责任。“技术味道”的缺失,是目前技术团队存在的最大问题。特别是做业务开发的技术团队,如果管理者完全不关心技术细节,绩效完全和业务 KPI 绑定,就会导致工程师们整天只会写 if-else 的业务代码,得不到技术上的成长。在这样的技术团队,团队的战斗力和凝聚力都会每况愈下。
目标管理的常见手段有关键绩效指标(Key Performance Index,KPI)和目标与关键成果(Objectives Key Results,OKR)两种方法。相比较而言,一味地追求 KPI,可能会导致短视;OKR 更注重短期利益和长期战略之间的平衡。
简单来说,Manager 是管理事务,是控制和权威;而 Leader 是领导人心,是引领和激发。 Leader 要做一些 Manager 的管理事务,但是管理绝对不是 Leader 工作的全部。
P 线:Profession,专业线。M 线:Manager,管理线。
只有和团队建立了情感链接和信任关系,才能更好地开展工作。 我们在公司工作,实际上是在给两个账号存钱:一个是绩效货币(Performance Currency),这是对事的; 另一个是关系货币(Relationship Currency),这是对人的。 所有的判断都有人的主观因素在里面,因此第二个货币也很重要。
在此提醒一点,搞好关系并不是拉帮结派,还是那句话:“动机至善,了无私心”。 我们做事情的出发点必须要是正的、善的。在这个大前提下,我们可以积极地拓展自己的人脉关系和影响力。
做一个 Leader 不容易,因为你不仅要管好自己,还要成就他人。做一个技术 Leader 更不容易,因为技术的发展日新月异,你没有退路,如果不持续学习,你就会落伍;如果不深入技术细节,你就很难赢得下属的尊重。
CQRS 命令查询分离(Command Query Separation,CQS)最早是 BetrandMeyer(Eiffel 语言之父,OCP 的提出者)提出的概念,其基本思想在于任何一个对象的方法可以分为以下两类。
使用了 CQRS 之后,我们能够把读模型和写模型完全分开,从而可以优化读操作和写操作。除了性能提升,CQRS 还让代码库更清晰简洁,更能体现出领域,易于维护。
https://github.com/alibaba/COLA