开篇废话
这么多年一直在写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);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
// 重构后
private T injectExtension(T instance) {
if (objectFactory == null) {// 提出来,单独判空,尽快return
return instance;
}
try {
for (Method method : instance.getClass().getMethods()) {
// 3个条件,单独提出boolean变量,并且从名字可以看出要判断啥东西,这个地方不怕名字过长。表意很重要
boolean isSingleParamPublicSetMethod = method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers());
if (!isSingleParamPublicSetMethod) {// 反向判断,阅读时会很容易理解,要不然得仔细阅读3个条件,还要适当猜一猜。不满足条件直接continue换下一个,简单明了。
continue;
}
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);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
案例5(if反写)
// Dubbo 源码,ExtensionLoader.isMatchGroup
// 这个案例非常简单,我拿出来是想说明,编码过程要注重每一个简单的细节。
private boolean isMatchGroup(String group, String[] groups) {
if (group == null || group.length() == 0) {
return true;
}
// 这个地方还是反写一下,会更优。
// 在CodeReview的时候,类似这种情况,coder就会说:我这么写没啥,也挺清晰,没必要那么计较。
// 是很清晰,大家也一眼就能看懂啥意思。
// 我想说的是,一大段复杂的代码是有由N个小的简单的代码拼凑出来的,请把每一个小的细节做到位。把每个小的细节都写的优雅了,你的整个架构,整个系统才会变得更加完美。
if (groups != null && groups.length > 0) {
for (String g : groups) {
if (group.equals(g)) {
return true;
}
}
}
return false;
}
// 重构后
private boolean isMatchGroup(String group, String[] groups) {
if (group == null || group.length() == 0) {
return true;
}
if (groups == null || groups.length == 0) {// 反写判断,直接return,大家一看就懂。
return false;
}
for (String g : groups) {
if (group.equals(g)) {
return true;
}
}
return false;
}
案例6(减少不必要的else)
// Dubbo 源码,RegistryProtocol.getFilteredKeys
// 这个类本身很简单,但是细纠,会觉得不太符合快速结束的原则
private static String[] getFilteredKeys(URL url) {
Map<String, String> params = url.getParameters();
if (params != null && !params.isEmpty()) {// if应该要反向判断
List<String> filteredKeys = new ArrayList<String>();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (entry != null && entry.getKey() != null && entry.getKey().startsWith(Constants.HIDE_KEY_PREFIX)) {
filteredKeys.add(entry.getKey());
}
}
return filteredKeys.toArray(new String[filteredKeys.size()]);// 这边没有必要给长度,给一个空数组即可
} else {// 这个else是多余的
return new String[]{};
}
}
// 重构后
private static String[] getFilteredKeys(URL url) {
Map<String, String> params = url.getParameters();
if (params == null || params.isEmpty()) {// if反写,快速结束,省略了else
return new String[]{};
}
List<String> filteredKeys = new ArrayList<>();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (entry != null && entry.getKey() != null && entry.getKey().startsWith(Constants.HIDE_KEY_PREFIX)) {
filteredKeys.add(entry.getKey());
}
}
return filteredKeys.toArray(new String[0]);
}
案例7(减少不必要的else)
// Dubbo 源码,AbstractCodec.isClientSide
// 这个案例对于else的使用有很明显,是不必要的。
protected boolean isClientSide(Channel channel) {
String side = (String) channel.getAttribute(Constants.SIDE_KEY);
if ("client".equals(side)) {
return true;// 都return了,还搞啥else呢?
} else if ("server".equals(side)) {
return false;// 都return了,还搞啥else呢?
} else {
InetSocketAddress address = channel.getRemoteAddress();
URL url = channel.getUrl();
boolean client = url.getPort() == address.getPort()
&& NetUtils.filterLocalHost(url.getIp()).equals(
NetUtils.filterLocalHost(address.getAddress()
.getHostAddress()));
channel.setAttribute(Constants.SIDE_KEY, client ? "client"
: "server");
return client;
}
}
// 重构后
protected boolean isClientSide(Channel channel) {
String side = (String) channel.getAttribute(Constants.SIDE_KEY);
if ("client".equals(side)) {
return true;// 很明显,那个else是没有必要的
}
if ("server".equals(side)) {
return false;// 很明显,那个else是没有必要的
}
InetSocketAddress address = channel.getRemoteAddress();
URL url = channel.getUrl();
boolean client = url.getPort() == address.getPort()
&& NetUtils.filterLocalHost(url.getIp()).equals(
NetUtils.filterLocalHost(address.getAddress()
.getHostAddress()));
channel.setAttribute(Constants.SIDE_KEY, client ? "client" : "server");
return client;
}
案例8(减少不必要的else)
// Dubbo 源码,ExchangeCodec.encodeResponse(方法中的代码片段)
if (t instanceof IOException) {
throw (IOException) t;
} else if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else if (t instanceof Error) {
throw (Error) t;
} else {
throw new RuntimeException(t.getMessage(), t);
}
// 重构后
if (t instanceof IOException) {
throw (IOException) t;
}
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
}
if (t instanceof Error) {
throw (Error) t;
}
throw new RuntimeException(t.getMessage(), t);
反向思维带来的好处
① 从思维上:把正向思路无限逼近一个狭小的空间,代码越往后,分支越少,思路越明了。使其变得更加透彻。
② 从代码结构上:把代码结构和思路都变得简单,降低if
for
等等的层次。
注意:并不是所有时候if反写都能带来好处,要看场景,可以参考案例4中,我没有做if
反写的地方。适当平衡,平衡很重要。
第5个案例非常有代表性,代码很简单,我想就此再强调下。一大段复杂的代码是有由N个小的简单的代码拼凑出来的,请把每一个小的细节做到位。把每个小的细节都写的优雅了,你的整个架构,整个系统才会变得更加完美。
2.减少累赘的代码
Java足够严谨,但是不够简洁,想要把Java代码写的优雅,不要写费代码,不要拖沓。看案例
案例1(适当使用三目运算)
// Dubbo 源码,URL构造方法
public URL(String protocol, String username, String password, String host, int port, String path, Map<String, String> parameters) {
if ((username == null || username.length() == 0)
&& password != null && password.length() > 0) {
throw new IllegalArgumentException("Invalid url, password without username!");
}
this.protocol = protocol;
this.username = username;
this.password = password;
this.host = host;
this.port = (port < 0 ? 0 : port); // 比大小没必要这么写
// trim the beginning "/"
while (path != null && path.startsWith("/")) {
path = path.substring(1);
}
this.path = path;
// 下面对parameters的判断处理过程有点累赘
if (parameters == null) {
parameters = new HashMap<String, String>();
} else {
parameters = new HashMap<String, String>(parameters);
}
this.parameters = Collections.unmodifiableMap(parameters);
}
// 重构后
public URL(String protocol, String username, String password, String host, int port, String path, Map<String, String> parameters) {
if ((username == null || username.length() == 0) && password != null && password.length() > 0) {
throw new IllegalArgumentException("Invalid url, password without username!");
}
this.protocol = protocol;
this.username = username;
this.password = password;
this.host = host;
this.port = Math.max(port, 0);// 比大小,这么写更加清晰
this.parameters = Collections.unmodifiableMap(parameters == null ? new HashMap<>() : new HashMap<>(parameters));// 这边用三目运算符可以减少5行代码,而且也不难理解
while (path != null && path.startsWith("/")) {
path = path.substring(1);
}
this.path = path;
}
案例2(Map.computeIfAbsent挺好用的)
// Dubbo 源码,AbstractRegistry.subscribe(方法中的代码片段)
public void subscribe(URL url, NotifyListener listener) {
// ---------------------省略一些代码------------------------
Set<NotifyListener> listeners = subscribed.get(url);
if (listeners == null) {
subscribed.putIfAbsent(url, new ConcurrentHashSet<NotifyListener>());
listeners = subscribed.get(url);
}
listeners.add(listener);
}
// 重构后
public void subscribe(URL url, NotifyListener listener) {
// 利用computeIfAbsent,只有当key不存在时,后续的表达式才会被执行。不会影响性能
// 其次得到的必然是最新的值。多写几次会比较适应这样的写法
subscribed.computeIfAbsent(url, key -> new ConcurrentHashSet<>()).add(listener);
}
案例3
// Dubbo 源码,NettyClient.doOpen(方法中的代码片段)
// 问题1:getTimeout()如果大于3000,这个方法就会被调用2次。
// 问题2:用Math.max(3000, getTimeout())就能解决,if判断没有意义
protected void doOpen() throws Throwable {
// ---------------------省略一些代码------------------------
if (getTimeout() < 3000) {
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
} else {
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getTimeout());
}
// ---------------------省略一些代码------------------------
}
// 重构后
protected void doOpen() throws Throwable {
// 使用Math.max(3000, getTimeout())就能变成一行代码。
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, Math.max(3000, getTimeout()));
}
案例4
// Dubbo 源码,Decodeable
// 从Dubbo的源码中不小心找到的,其他所有的接口声明都没有这问题,应该是不小心漏了
public interface Decodeable {
// 这个案例拿出来,还是强调细节,可能很多人觉得多写个public没什么,那怎么不把abstract也写上呢?
// 说到第还是习惯问题,不要写没有必要的废代码
// 这些细节idea是可以检查出来的,或者发布前全量扫描下代码也是可以修正的
public void decode() throws Exception;
}
// 重构后,接口中的行为声明不要带上public
public interface Decodeable {
void decode() throws Exception;
}
案例5
// Dubbo 源码,RpcResult
// 这个类中attachments是不可能为null的。这个从业务层面保证了的。所以这个方法中的判空是没有意义的。属于非代码
private Map<String, String> attachments = new HashMap<String, String>();
public void addAttachments(Map<String, String> map) {
if (map == null) {
return;
}
// attachments不可能为null,判空没有意义。如果真为空了,反而要看看这个类的结构是否哪边出问题了。
if (this.attachments == null) {
this.attachments = new HashMap<String, String>();
}
this.attachments.putAll(map);
}
// 重构后,删除了对attachments的判空操作
private Map<String, String> attachments = new HashMap<String, String>();
public void addAttachments(Map<String, String> map) {
if (map == null) {
return;
}
this.attachments.putAll(map);
}
有人说,Java
代码就像是老太太的裹脚布,又臭又长。我内心不想认同,当我接触了Python
,GO
语言后,对比发现有时候的确是这样。但我任然喜欢写Java。于此同时,我希望尽可能不要写费代码。合理利用封装,继承(不要排斥继承),多态,组合,完全能写出优雅的代码。
3.谈谈命名问题
不是谈帕斯卡,骆驼命名等问题。而是说说业务模块和技术模块的一些命名优劣。以订单模块为例,我会给出OrderService
和OrderDao
中一些基本的接口定义。
下面先列出,我认为不太友好的服务接口定义方式:
// 我先给出DAO的定义,这个DAO的定义没有问题的,主要看下面的OrderService的定义
public interface OrderDao {
// 根据用户Id和状态查询订单
List<Order> selectByUserIdAndState(String userId, List<Integer> orderStates);
// 根据订单Id查询订单
Order selectByPrimaryKey(String id);
// 插入订单数据
int insert(Order order);
// 更新订单状态
int updateStateById(String id, Integer state);
}
Bad Case
// 这个接口定义是我自己编出来的,虽然不完全真实。但我想大家项目中一定见到过在业务逻辑中这么与定义方法名字。
// 出现了insert,update,select等等技术行为动作
// 看着像是DAO的定义,但有不完全像,下面注释具体说问题。
public interface OrderService {
// 根据用户Id和状态查询订单
// 问题① select不应该出现在业务层,可以使用query代替,偏业务化
// 问题② state状态不应该暴露给外面,订单状态应该有订单模块内部控制
List<Order> selectOrdersByUserIdAndState(String userId, int state);
// 根据订单Id查询订单
// 问题① select不应该出现在业务层,可以使用query代替,偏业务化
Order selectOrderById(String orderId);
// 插入订单信息
// 问题① insert不应该出现在业务层,从业务角度我们可以说初始化订单,生成订单,创建订单等等,但是不要用插入这种数据库专用名词
boolean insertOrder(String userId, String price, String productId);
// 更新订单状态
// 问题① update不应该出现在业务层,我们可以换个方式去暴露接口
// 问题② state状态不应该暴露给外面,订单状态应该有订单模块内部控制
void updateOrderState(String orderId, int state);
}
Good Case
// 优化过后
public interface OrderService {
// 查询用户初始化订单
// ① 把select修改成了偏业务化的query,也不再有by,userId,state这种数据库关键字或者字段化的概念了
// ② 把订单状态对其他模块屏蔽了,其他模块只需要向订单模块发起查询初始化订单的请求,不再需要知道初始化订单状态是啥数字
List<Order> queryUserInitOrders(String userId);
// 查询用户完成订单
// ① 把select修改成了偏业务化的query,也不再有by,userId,state这种数据库关键字或者字段化的概念了
// ② 把订单状态对其他模块屏蔽了,其他模块只需要向订单模块发起查询完成订单的请求,不再需要知道完成订单状态是啥数字
List<Order> queryUserFinishOrders(String userId);
// 根据订单Id查询订单
// ① 把select修改成了偏业务化的query,也不再有by,orderId这种数据库关键字或者字段化的概念了
Order queryOrder(String orderId);
// 初始化订单
// ① 插入订单的概念被修改成初始化订单,更偏业务化,而不是机械动作
boolean initOrder(String userId, String price, String productId);
// 支付成功通知
// ① 更新订单状态的行为不再交由外部模块控制,而是由订单模块内部自己控制。
// 订单模块只管给外部提供支付成功的通知接口,其他模块不关心通知过后订单模块干了啥,接到通知,是否要修改订单状态,订单状态要修改成什么,是订单模块自己的事情。
void paySuccessNotice(String orderId);
// 支付失败通知
// ① 更新订单状态的行为不再交由外部模块控制,而是由订单模块内部自己控制。
// 订单模块只管给外部提供支付失败的通知接口,其他模块不关心通知过后订单模块干了啥,接到通知,是否要修改订单状态,订单状态要修改成什么,是订单模块自己的事情。
void payErrorNotice(String orderId);
}
// 根据上述订单服务,简单列了几个基本的订单状态,不是很严谨。
public enum OrderStateEnum {
INIT(0),// 初始化
PAY_ING(1),// 支付中
ORDER_FINISH(2),// 订单完成
ORDER_CLOSE(3),// 订单关闭
;
private int state;
OrderStateEnum(int state) {
this.state = state;
}
public int getState() {
return state;
}
}
在三层架构(展示层,业务逻辑层,数据访问层)下,我们编码时应该要充分考虑每一层各自的职责。业务逻辑层的重要性谁都清楚。但很多时候,我经常看到业务逻辑层的方法命名长得跟DAO
类似。特别是updateXXX
,insertXXX
。分层或者分模块的目的就是为了解耦合,各自职责独立,我们应该严格遵守迪米特法则,把对外暴露的接口信息降到最低。
有朋友会说,queryUserInitOrders
和queryUserFinishOrders
分开了,那如果订单表有10个状态,岂不是得写10个方法。我想说对,就是这样。封装10个不同状态的查询接口是订单模块细粒度提供服务的职责。
如果想要查询某用户下所有终止状态(完成和关闭)的订单,还需要封装queryUserEndOrders
的服务,这算是门面模式。
绝对业务化的命名方式有几个好处。
① 模块多了,各个模块的业务方法多了,维护业务+数据库混杂命名的方法,会晕的。维护纯业务命名的方法会舒服一些。
② 其他模块的开发者并不想知道你内部模块实现细节,干嘛要在命名中带有隐晦的提现。
③ 做一个系统,我一般定先定流程骨架,业务模块只写接口定义,不写实现,业务模块的命名对我后期来完善方法具体实现有指导价值。
④ 显得专业,不,是真的专业。
4.善用Enum
Enum
真的是个好东西,香。
用enum + spring实现策略模式
在实际的项目中,一定会碰到很多需要需要横向扩展的场景,比如:技术方面的,Dubbo
的负载均衡策略,协议策略,序列化策略等等。业务方面的,支付策略(支付宝,微信,余额),登录策略(用户名密码,验证码)。这些场景都是在一定范围内可以枚举,并且需要有横向扩展的能力,需要符合开闭原则。
Dubbo
用了自己的SPI
,目的是为开发者做二次开发留下很好的扩展口,这方面知识可以参考文档:Dubbo SPI。系统开发或者业务开发,没有必要提供二次开发扩展口的情况下,可以用Enum
来做到比较优雅的实现。
处理方式:
① Enum
作为策略的列表枚举
② Spring
的bean
工场可以替代SPI
作为策略实现类动态获取的工具。
下面分别以Dubbo
的负载策略和支付策略为案例说明下这种方式。
Dubbo
的负载均衡策略
// 负载均衡,接口
public interface LoadBalance {
// 从多个待选执行列表中选出一个
Invoker select(List<Invoker> invokers);
}
// 负载均衡策略,参照dubbo的4种。只是示意
public enum LoadBalanceStrategy {
CONSISTENT_HASH("consistentHashLoadBalance"),//一致性 hash 算法
LEAST_ACTIVE("leastActiveLoadBalance"),//最小活跃数负载均衡
RANDOM("randomLoadBalance"),//加权随机算法
ROUND_ROBIN("roundRobinLoadBalance"),//加权轮询负载均衡
;
private String beanId;
LoadBalanceType(String beanId) {
this.beanId = beanId;
}
public String getBeanId() {
return beanId;
}
}
// 四种实现方式,具体实现都省略了。只是表意。
public class RoundRobinLoadBalance implements LoadBalance {//加权轮询负载均衡
}
public class RandomLoadBalance implements LoadBalance {//加权随机算法
}
public class LeastActiveLoadBalance implements LoadBalance {//最小活跃数负载均衡
}
public class ConsistentHashLoadBalance implements LoadBalance {//一致性 hash 算法
}
// 这个类作为对其他模块暴露的入口
public class LoadBalanceDispatcher {
@Autowired
private ApplicationContext applicationContext;
public Invoker doSelect(LoadBalanceStrategy strategy, List<Invoker> invokers) {
// 通过applicationContext动态获取bean对象
LoadBalance loadBalance = applicationContext.getBean(strategy.getBeanId(), LoadBalance.class);
return loadBalance.select(invokers);
}
}
支付工具策略(我上个项目的真实代码)
public interface IPayProcessor {
// 生成支付订单
String createPayOrder(Order order, String userIp);
// 第三方支付结果的通知
boolean payStateNotice(Map<String, String> notifyMap);
// 查询支付订单
Integer queryPayOrder(PayOrder payOrder);
// 退款
boolean refundPayOrder(String payId);
// 关闭交易
boolean closePayOrder(String payId);
}
// 对,你没看错,我真的用了中文作为enum的value。
// 我们在适当的场景下,有这么做的必要性,别碰我。
public enum PAY_TOOL_TYPE {
支付宝app支付(1, "aliPayProcessor"),
微信app支付(2, "wxappPayProcessor"),
微信小程序支付(3, "wxmpPayProcessor"),
微信H5支付(4, "wxmwebPayProcessor"),
微信扫码付(7, "wxNativePayProcessor"),
微信快应用支付(8, "wxQuickAppPayProcessor"),
支付宝H5支付(5, "aliWapPayProcessor"),
支付宝扫码付(6, "aliScanCodePayProcessor"),
;
private int type;
private String beanId;
PAY_TOOL_TYPE(int type, String beanId) {
this.type = type;
this.beanId = beanId;
}
public int getType() {
return type;
}
public String getBeanId() {
return beanId;
}
}
上述案例是真实项目,我个人特别喜欢这种方式去使用Enum
用enum开定义错误码
阿里规约泰山版中定义了错误码列表。我之前很多项目都是用常量类来定义一堆错误码,并且加上中文注释,用的时候可以方便查看。类似于下面这样。
public class ErrorCodeConstant {
/**
* 一切 ok
*/
public static final String A0000 = "A0000";
/**
* 用户端错误
*/
public static final String A0001 = "A0001";
/**
* 用户注册错误
*/
public static final String A0002 = "A0002";
}
用时间长了,总感觉不舒服。在代码中看到的永远是一堆编号,得点进去才能看见中文解释。怎么能在代码中直接看见中文?并且在接口out出系统时转换成编码呢?用Enum
。我在项目中尝试了,因为用了中文,称不上优雅,但很舒服。下面介绍下。
// 用Enum替代了常量类
// 使用了中文,在代码中不管是抛出点,还是拦截点,都能看到
// 我考虑过要使用英文,但是看到满屏的错误码,果断放弃了,如果真用了满屏的英文,还不如来点错误码数字了。。。
public enum EXCEPTION_MESSAGE {
交易成功("0000"),
系统错误("0001"),
请求参数有误("0002"),
用户token失效("0006"),
;
private String code;
EXCEPTION_MESSAGE(String code) {
this.code = code;
}
public String getCode() {
return code;
}
// 这个方法用来对比某个exception实例是否与当前这个相同
// 实际项目中用起来,可读性很高。看下面的演示
public boolean isMessage(MinifException e) {
return this.name().equals(e.getMessage());
}
}
// throw 异常的地方
throw new MinifException(EXCEPTION_MESSAGE.有座位已被抢_赶紧再选个座位下单吧);
try {
filmOrderService.filmLockSeat(orders, verticalOrderFilm, bestSeats);
} catch (MinifException e) {
// 做异常判断,并且转换。使用isMessage方法来判断。可读性高
// 都是直接来中文,容易理解和维护
if (EXCEPTION_MESSAGE.有座位已被抢_赶紧再选个座位下单吧.isMessage(e)) {
throw new MinifException(EXCEPTION_MESSAGE.有座位被抢重新选座);
}
if (EXCEPTION_MESSAGE.异常未释放的座位超出了最大值_请等待15分钟或主动释放座位.isMessage(e)) {
throw new MinifException(EXCEPTION_MESSAGE.未释放座位超出最大值);
}
throw new MinifException(EXCEPTION_MESSAGE.锁定座位失败);
}
对于Enmu
,介绍了2种用途,关于这部分,只是我的个人经验。能解决问题的方法才是好方法。
5.说说this
不是想说this
的用法特点,而是想说对私有方法调用时,希望可以加上this
关键字。有助于后期代码的阅读。当然,这只是我的个人习惯和观点。说说为什么,来看看代码。
// Dubbo 源码,DefaultFuture.setCallback
// invokeCallback是一个私有方法,但是我第一眼看这个方法,无法直接明白那行代码在调用一个私有方法,我会考虑它可能是本类的方法,或者是父类的方法,如果不是在IDE中,我还得看看它是不是静态方法。我需要1秒的思考时间。
// 为了减少这1秒的思考,我喜欢加上辨识度更高的this关键字。
public void setCallback(ResponseCallback callback) {
if (isDone()) {
invokeCallback(callback);// 如果我没在这边标注,你能知道invokeCallback()是在调用一个私有方法吗?
} else {
boolean isdone = false;
lock.lock();
try {
if (!isDone()) {
this.callback = callback;
} else {
isdone = true;
}
} finally {
lock.unlock();
}
if (isdone) {
invokeCallback(callback);// 如果我没在这边标注,你能知道invokeCallback()是在调用一个私有方法吗?
}
}
}
// 加上了this关键字
// 我不用思考就会知道它大概率是本类私有方法。
// 而且this关键字在IDE中的辨识度很高,直接就可以看到它是个方法,而不是个普通变量
// 我只是在大部分情况下,不希望在看代码时有过多的卡顿时间
public void setCallback(ResponseCallback callback) {
if (isDone()) {
this.invokeCallback(callback);
} else {
boolean isdone = false;
lock.lock();
try {
if (!isDone()) {
this.callback = callback;
} else {
isdone = true;
}
} finally {
lock.unlock();
}
if (isdone) {
this.invokeCallback(callback);
}
}
}
关于加this
关键字对本地私有方法的前缀。没有很明显的案例。不过我一直坚持这么做,我认为效果很好。
一些随机抽取的案例
案例1(没有必要的hash浪费)
// Dubbo 源码,SimpleDataStore.get
// 如果containsKey为true,则会出现2次hash操作(containsKey一次,get操作再一次)。浪费
public Object get(String componentName, String key) {
if (!data.containsKey(componentName)) {
return null;
}
return data.get(componentName).get(key);
}
// 重构后,用一个零时变量接一下,就不需要做2次hash了
public Object get(String componentName, String key) {
ConcurrentMap<String, Object> value = data.get(componentName);
if(value == null) {
return null;
}
return value.get(key);
}
案例2
// Dubbo 源码,ExtensionLoader.loadResource
// 这个方法写的挺奇怪的。两层try,没必要的。
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
try {// 对于资源处理,没有必要使用2层try
String line;
while ((line = reader.readLine()) != null) {
final int ci = line.indexOf('#');
if (ci >= 0) line = line.substring(0, ci);
line = line.trim();
if (line.length() > 0) {// 这边用if反写的方式,会更加清晰
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
}
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}
// 重构后
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
BufferedReader reader;// 资源在外面申明,当然java7的方式放在try的括号中也是可以的。
try {
reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
String line;
while ((line = reader.readLine()) != null) {
final int ci = line.indexOf('#');
if (ci >= 0) {
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() <= 0) {// if反写,直接continue
continue;
}
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
} finally {// 正常关闭资源
reader.close();
}
}
案例3(if内容体,任何时候都加上大括号)
// Dubbo 源码,ExtensionLoader.getExtensionLoader
// 前面2个if都是的内容体都是单行的。第二个加上了大括号,第一个却没有加
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null)// 没有加大括号
throw new IllegalArgumentException("Extension type == null");
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
// 重构后
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {// 任何时候都加上大括号。有助于阅读
throw new IllegalArgumentException("Extension type == null");
}
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
案例4(来一段复杂的)
// Dubbo 源码,ServiceConfig.doExportUrlsFor1Protocol(方法太大,截取了一段比较复杂的来搞一搞)
// 我数了下,最深12层。这段代码不在里面瞎写了,太复杂,影响阅读
if (methods != null && !methods.isEmpty()) {
for (MethodConfig method : methods) {
appendParameters(map, method, method.getName());
String retryKey = method.getName() + ".retry";
if (map.containsKey(retryKey)) {
String retryValue = map.remove(retryKey);
if ("false".equals(retryValue)) {
map.put(method.getName() + ".retries", "0");
}
}
List<ArgumentConfig> arguments = method.getArguments();
if (arguments != null && !arguments.isEmpty()) {
for (ArgumentConfig argument : arguments) {
// convert argument type
if (argument.getType() != null && argument.getType().length() > 0) {
Method[] methods = interfaceClass.getMethods();
// visit all methods
if (methods != null && methods.length > 0) {
for (int i = 0; i < methods.length; i++) {
String methodName = methods[i].getName();
// target the method, and get its signature
if (methodName.equals(method.getName())) {
Class<?>[] argtypes = methods[i].getParameterTypes();
// one callback in the method
if (argument.getIndex() != -1) {
if (argtypes[argument.getIndex()].getName().equals(argument.getType())) {
appendParameters(map, argument, method.getName() + "." + argument.getIndex());
} else {
throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
}
} else {
// multiple callbacks in the method
for (int j = 0; j < argtypes.length; j++) {
Class<?> argclazz = argtypes[j];
if (argclazz.getName().equals(argument.getType())) {
appendParameters(map, argument, method.getName() + "." + j);
if (argument.getIndex() != -1 && argument.getIndex() != j) {
throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
}
}
}
}
}
}
}
} else if (argument.getIndex() != -1) {
appendParameters(map, argument, method.getName() + "." + argument.getIndex());
} else {
throw new IllegalArgumentException("argument config must set index or type attribute.eg: <dubbo:argument index='0' .../> or <dubbo:argument type=xxx .../>");
}
}
}
} // end of methods for
}
// 重构后,最深变成了6层
// 多使用循环控制continue,break
if (methods != null && !methods.isEmpty()) {
for (MethodConfig method : methods) {
appendParameters(map, method, method.getName());
String retryKey = method.getName() + ".retry";
if (map.containsKey(retryKey)) {
String retryValue = map.remove(retryKey);
if ("false".equals(retryValue)) {
map.put(method.getName() + ".retries", "0");
}
}
List<ArgumentConfig> arguments = method.getArguments();
if (arguments == null || arguments.isEmpty()) {
continue;
}
for (ArgumentConfig argument : arguments) {
boolean isArgumentTypeEmpty = argument.getType() == null || argument.getType().length() == 0;
boolean argumentHasIndex = argument.getIndex() != -1;
if (isArgumentTypeEmpty && !argumentHasIndex) {
throw new IllegalArgumentException("argument config must set index or type attribute.eg: <dubbo:argument index='0' .../> or <dubbo:argument type=xxx .../>");
}
if (isArgumentTypeEmpty) {
appendParameters(map, argument, method.getName() + "." + argument.getIndex());
continue;
}
Method[] methods = interfaceClass.getMethods();
if (methods == null || methods.length == 0) {
continue;
}
for (int i = 0; i < methods.length; i++) {
String methodName = methods[i].getName();
if (!methodName.equals(method.getName())) {
continue;
}
Class<?>[] argtypes = methods[i].getParameterTypes();
if (argument.getIndex() != -1) {
if (!argtypes[argument.getIndex()].getName().equals(argument.getType())) {
throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
}
appendParameters(map, argument, method.getName() + "." + argument.getIndex());
continue;
}
for (int j = 0; j < argtypes.length; j++) {
Class<?> argclazz = argtypes[j];
if (!argclazz.getName().equals(argument.getType())) {
continue;
}
if (argument.getIndex() != -1 && argument.getIndex() != j) {
throw new IllegalArgumentException("argument config error : the index attribute and type attribute not match :index :" + argument.getIndex() + ", type:" + argument.getType());
}
appendParameters(map, argument, method.getName() + "." + j);
}
}
}
}
}
总结
很多东西,我不太知道怎么通过案例去表达。比如:怎么重构业务架构(代码量太大,东西太多,太碎,不知道咋写,写出来会乱),适当减少静态方法的使用,怎么用实体会话服务作为主线来构建架构。类似于这些东西过于偏思想,我希望写的东西是思想与实际案例结合的,是能从具体上去感受的,而不是只有抽象。
先说这么多,后面有想法继续写。