开篇废话
这么多年一直在写Java
代码。在编码规范和代码优雅问题上一直都有自己的一套理论,脑中有大量有型的案例。习惯在周末的时候独自在家大批量的重构项目中的代码,看到不规范的代码就有要修改它的冲动。对代码有洁癖,也喜欢重构。一个周末重构上百个类,真的挺爽的。重构的多了,觉得挺没意思,因为感觉都在重复同样的工作,都不用动脑子,重构的过程感觉是在休息和偷懒。。。
从理性上,我觉得把代码写好看,注意是要好看。是每个程序员必备的基本技能。
从感性上,我能理解有大量的coder写出来的代码像狗啃的,毕竟对代码优雅度的追求,需要一个过程。
花了几天时间把阿里规约华山版,看了好几遍。我很喜欢它的大量的条条框框。我觉得很有价值。也要求我的团队按照规范去写。还去阿里云做了一下认证考试。98分,也不知道哪一题错了。
对于这套规范,我很认同。但总觉的少了点什么。规范是阿里在大量的项目工程实践中抽象出来的产物,是结果,而缺少了一些过程。很大一部分人无法从结果直接体会到某些条目的价值和重要性,也就很难从内心深处去深刻理解,并且规范自己。
每个有经验的coder都是通过代码堆出来的(并不是在说不用动脑子),实际上就是通过大量的案例来告诉自己选择哪种写法是比较好的,选着哪种写法不太优,选择哪种写法是傻X行为。我希望能写一些我认为有意思的案例或者想法说明某些条条框框的准则的价值。
Java
本身就有一些默认的潜在行业规范,阿里规约算是把行业规范更加明晰的写了出来,并且加入了更多的细节。从实践角度,我认为每个项目都有自己的业务特色,项目组内一般都会有一些自己特定的规范,包括各个公司可能也有一些自己特定的规范,当然很多还是借鉴了行业规范。如果按照重要程度排序的话,应该是这样的:项目组内规范 > 公司规范 > 阿里规约(行业规范)。
举个例子,一个语义解析的项目,要在一个Enum
中定义所有的100个语义意图。比如:调整音量,关窗,开窗,打开XXX,退出,询问明星等等,一些列的意图。这个时候还在Enum
中把这些及其复杂的中文意图翻译成英文,然后变成大写再写进Enum
中,人读的时候还要脑子里再翻译下,当然可以写注释。我想说,这是吃饱了撑得,直接用中文不行吗。在这个项目的业务层面,这样子去约定有它的必要性,哪怕违反了一些我们常规的基本准则。有些朋友可能会说,在Enum
中写死上百个意图本身就不合理,应该放到数据库或者缓存。我们不在这边去纠结某个细节,因为类似于这种问题是否合理,需要结合场景,需要平衡优劣和得失。我举这么个例子,只是想说明项目组内很多时候由于业务的特点需要违反一些普遍行规,但这种违反,不是大面积的,只会在某几个点上,并且每一个点都有严格的组内规范去说明并且执行。
经常在CodeReview的时候听到一句话:我这么写好像也没什么,我觉得也挺好看的,可能发生的一些影藏问题在咋们的场景中不会发生,要不就这样不改了。不知道大家是否也听过,或者有同样的感受。我个人不喜欢,我希望在一个项目中,所有人写的代码就跟一个人写的一样,都严格遵循同一种写法,同一种规范。我希望达到的那种偏理想化的结果,仅靠阿里规约恐怕是不够的,举几个例子。
1.阿里规约指导大家用什么样的格式去命名方法,类,表,Enum
等等。但是没有告诉大家业务层的方法该怎么命名,技术层的方法该怎么命名。比如,我们能在OrderServiceImpl
类中看到某个私有方法叫:insertOrder
。这个方法很正常,也符合规范,但仔细一想,总觉业务层出现insert动作很奇怪。暂不细说,后面会有案例详聊。
2.阿里规约没有告诉大家,什么时候该用静态方法,什么时候该用成员方法,什么时候该用单例。
3.阿里规约限定了说,每个方法都不要超过80行,但没有告诉大家怎么把一个500行的方法重构成10个小方法来复用。
我说这些例子,不是想说明阿里规约不好,而是说阿里规约有它的侧重点。它已经做的很好。如果把编码比作是练武。那阿里规约是在教大家学习招式和部分内功,而我举得案例,比如业务层和技术层的方法分别该怎么命名,这些属于内功。它真的需要大量代码,时间,还有反思才能精进。
以上说了很多废话哈,下面进入正题来看看我是否能带来一些体现内功的东西。
对于我提出的每个想法,我都会举出多个案例,来说明。每个案例都会有重构前和重构后的代码来展示。每个案例之间会用分割线隔离开。大量的描述和解释,尽量放在代码中作为注释出现。尽量做到每个案例都是有某些框架的源代码,都是有出处的,如果我写了大量的注释让大家没法看了,请去找框架的源码。
上料
1.反向思维,尽快结束原则
我认为是方法或者函数最有指导价值的一个思想,放在第一个说。
要达到反向思维,尽快结束目的,我总结下3种方式
① if
判断反写,尽快return
或者throw
(最简单,最实用)
② 减少不必要的else
,能不用则不用
③ 在循环中多使用break
,continue
等流程控制方式
案例1(if反写,多用流程控制语句)
// Dubbo 源码,RegistryDirectory.destroyUnusedInvokers
// 这个方法干了啥?把old列表跟new列表做一遍比对,把new列表中不存在元素,都做destory操作。也就是,循环old列表,然后逐个看new列表有没有,没有就记录到删除列表,然后循环删除列表逐个做destory操作
// 有哪些问题?看下面对应点的描述。
private void destroyUnusedInvokers(Map<String, Invoker<T>> oldUrlInvokerMap, Map<String, Invoker<T>> newUrlInvokerMap) {
if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
destroyAllInvokers();
return;
}
// check deleted invoker
List<String> deleted = null;// 这个列表真的有必要吗?为什么不能在循环的过程中,比对命中就做destory操作
if (oldUrlInvokerMap != null) {
// 如果oldUrlInvokerMap为空,那下面deleted列表也必然为空,那为什么不做反向判断?当oldUrlInvokerMap == null,就直接return
Collection<Invoker<T>> newInvokers = newUrlInvokerMap.values();
for (Map.Entry<String, Invoker<T>> entry : oldUrlInvokerMap.entrySet()) {
if (!newInvokers.contains(entry.getValue())) {
if (deleted == null) {
// 这边做懒加载的价值真的不大,正面价值节省不了多少性能,负面却是影响阅读体验,还有,每次过来都得做空判断。
deleted = new ArrayList<String>();
}
deleted.add(entry.getKey());
}
}
}
if (deleted != null) {
// 如果上面不是懒加载,这边的这个判断就可以省了。
for (String url : deleted) {
if (url != null) {
// 这边的判空有意义嘛?上面add进来的key从业务角度不可能为空,就算要做空判断,也应该放到add的入口那边,add的入口判空,如果是null就别给加进来了。
Invoker<T> invoker = oldUrlInvokerMap.remove(url);
if (invoker != null) {
// invoker从业务上不可能为空,如果这边真的空了,那就是流程的上游有问题,应该尽快暴露,而不是通过判空来糊弄过去。
try {
invoker.destroy();
if (logger.isDebugEnabled()) {
logger.debug("destroy invoker[" + invoker.getUrl() + "] success. ");
}
} catch (Exception e) {
logger.warn("destroy invoker[" + invoker.getUrl() + "] faild. " + e.getMessage(), e);
}
}
}
}
}
}
// 重构后
// 上面的代码最深有5层,而重构过后最深只有2层。其次阅读起来也变得舒服多了。
private void destroyUnusedInvokers(Map<String, Invoker<T>> oldUrlInvokerMap, Map<String, Invoker<T>> newUrlInvokerMap) {
if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
// 这边没有问题,还是正常照抄。
destroyAllInvokers();
return;
}
if (oldUrlInvokerMap == null || oldUrlInvokerMap.size() == 0) {
// 如果old列表为空,后续操作就没有必要了。可以直接return。
return;
}
// 如果仔细去看代码,会发现这个地方其实跟map里面的key没有任何关系。要销毁的是value(invoker)
Collection<Invoker<T>> newInvokers = newUrlInvokerMap.values();
for (Invoker<T> invoker : oldUrlInvokerMap.values()) {
if (newInvokers.contains(invoker)) {
// 如果new列表还存在,则不做destory,那么就continue,去搞下一个咯。
continue;
}
try {
invoker.destroy();// 做实际的destory
if (logger.isDebugEnabled()) {
logger.debug("destroy invoker[" + invoker.getUrl() + "] success. ");
}
} catch (Exception e) {
logger.warn("destroy invoker[" + invoker.getUrl() + "] faild. " + e.getMessage(), e);
}
}
}
案例2(if反写)
// Dubbo 源码,ZookeeperRegistry.doUnsubscribe(不符合快速结束和减少if层次结构的原则)
// 有哪些问题?看下面对应点的描述。
protected void doUnsubscribe(URL url, NotifyListener listener) {
ConcurrentMap<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners != null) {
// 反向判断下,如果为空,不就可以直接return了嘛
ChildListener zkListener = listeners.get(listener);
if (zkListener != null) {
// 反向判断下,如果为空,不就可以直接return了嘛
if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
String root = toRootPath();
zkClient.removeChildListener(root, zkListener);// 下面加一行return,不就可以把else给省去了么
} else {
for (String path : toCategoriesPath(url)) {
zkClient.removeChildListener(path, zkListener);
}
}
}
}
}
// 重构后,会清晰很多,代码也整洁了,阅读感受会更好。
protected void doUnsubscribe(URL url, NotifyListener listener) {
Map<NotifyListener, ChildListener> listeners = zkListeners.get(url);
if (listeners == null || listeners.size() == 0) {
return;
}
ChildListener zkListener = listeners.get(listener);
if (zkListener == null || zkListener.size() == 0) {
return;
}
if (Constants.ANY_VALUE.equals(url.getServiceInterface())) {
String root = toRootPath();
zkClient.removeChildListener(root, zkListener);
return;
}
for (String path : toCategoriesPath(url)) {
zookeeperClient.removeChildListener(path, zkListener);
}
}
案例3(if反写)
// Dubbo 源码,ExtensionLoader.getAdaptiveExtension
public T getAdaptiveExtension() {
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
// 反向判断,是否可以尽快return
if (createAdaptiveInstanceError == null) {
// 反向判断,是否可以尽快return
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
// 反向判断,是否可以尽快return
try {
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
}
}
}
} else {
throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}
return (T) instance;
}
// 重构后,会有更好的阅读感受。
public T getAdaptiveExtension() {
Object instance = cachedAdaptiveInstance.get();
if (instance != null) {
// 反向判断,尽快return,思路更加清晰明了
return (T) instance;
}
if (createAdaptiveInstanceError != null) {
// 反向判断,尽快return,思路更加清晰明了
throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance != null) {
// 反向判断,尽快return,思路更加清晰明了
return (T) instance;
}
try {
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
}
}
return (T) instance;
}
案例4(if反写,多用流程控制语句)
// Dubbo 源码,ExtensionLoader.injectExtension
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
// 非空判断不可能throw异常,是否可以放到外面,并且反向判断
for (Method method : instance.getClass().getMethods()) {
// 这个if有3个条件,是否可以单独提取出来,并且反向判断
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
Class<?> pt = method.getParameterTypes()[0];
try {
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
}