Open Source, Open Future!
  menu
107 文章
ღゝ◡╹)ノ❤️

设计指南 置顶!

1、背景

程序开发有四个重要的板块:语言、设计、框架、产品。

  • 语言:是基本元素,是游戏规则,是构建程序世界的基石
  • 设计:规范、编程思想、方法论、设计模式、算法、最佳实践、寻找最优解
  • 框架:框架的作用犹如工业革命带来了机械化、自动化,大大提高了生产效率
  • 产品:中间件、操作系统、业务系统等
img

2、问题

🔶 现象一:

可能有不少人入门都会经历 👇 的流程 :

语言(java) --> 框架 (SSH)-->产品(mysql),后续几年的工作中在这三个板块上不断加强。

⚠️ 问题:

忽略了一个板块:设计。

🔶 现象二:

同样的功能,同样的产品,不同的团队去做,设计方案、代码实现等等是不一样的,可能差别会很大;最终导致开发周期、产品性能、稳定性、扩展性等方面天差地别。

在众多的方案中,如何能使我们团队的设计成为最优解?始终走在最前面。

🔶 现象三:

在实际的开发中,我们可能会遇到一些问题,感觉很棘手,没有思路,找不到突破点。

⚠️ 原因:

  • 这个问题可能确实很复杂难解
  • 我们的技术储备不够丰厚;一些问题之前没遇到过,在对应领域内也没有足够沉淀,就像一张白纸,会有点懵 😵

3、未来规划

将设计板块梳理出一系列文档,形成一套完善的体系。

每一层都有对应的设计思想,每一层分4大块:优雅、高效、应用、从0到1

img

期望达到的目标:

🚩 深厚的技术沉淀

🚩 能够写出优雅的代码,有设计感

🚩 能够写出高效的代码,性能强悍

🚩 有快速找到最优解的能力

🚩 批量化、流水线价值产出

4、第一层:语言

4.1、优雅

什么是好的代码?

Bjarne Stroustrup (C++语言发明者,《C++程序设计语言》作者):

  • 我喜欢优雅高效的代码,代码逻辑应当直截了当,叫缺陷难以隐藏;
  • 尽量减少依赖关系,使之便于维护;
  • 依据某种分层战略完善错误处理代码
  • 性能调至最优,省的引诱别人做没规矩的优化,搞出一堆混乱来。
  • 整洁的代码只做好一件事

为什么要写好代码?

破窗理论

窗户破损了的建筑让人觉得似乎无人照管,于是别人也再不关心。
他们放任窗户继续破损。最终自己也参与破坏活动,在外墙上涂鸦,任垃圾堆积。
一扇破损的窗户开辟了大厦走向倾颓的道路。
img

对于代码来说,也是一样的。如果一个项目,没有好的规范和设计,经过几年的迭代后,会越来越混乱,难以维护,就像一个废弃的房子一样,最终项目的命运可能是推倒重来或者直接黄了。

4.1.1、命名

4.1.1.1、作用

  • 好的命名可以提升代码的可读性
  • 引导我们更加深入地理解问题域,理清关键业务概念
  • 设计出更加符合业务语义、易于理解的系统

4.1.1.2、难点

在计算机科学中有两件难事:缓存失效和命名。   
                                                      --- Phil Karlton
起一个好名字应该很难,因为一个好名字需要把要义浓缩在一到两个词中。 

                              --- Joel Spolsky (Stack Overflow的创始人)

命名的过程本身就是一个抽象思考的过程;

在工作中,当我们不能给一个模块、一个对象、一个函数,甚至一个变量找到合适的名称时,往往说明我们对问题的 理解还不够透彻;

需要 重新去挖掘问题的本质 ,对问题域进行重新分析和抽象 ,有时还要 调整设计和重构代码 。因此,好的命名是我们写出好代码的基础。

4.1.1.3、原则

4.1.1.3.1、一致性

1、一个概念对应一个词

例如,get、find、query、fetch、retrieve等都可以表示查询,在同一个类中或多个类中没有统一,有多种写法,看起来就会很混乱。一些常见的约定:

操作约定
新增save
删除remove
修改update
查询(单个结果)get
查询(多个结果)list
分页查询page
统计count

2、对仗词

像 first / last 这样的对仗词就很容易理解;而像 fileOpen() 和 fClose() 这样的组合则不对称,容易使人迷惑。一些常见的对仗词组:

incrementdecrement
addremove
lockunlock
createdestroy
beginend
insertdelete
showhide
sourcetarget
nextprevious

3、后置限定词

表示计算结果的变量,例如总额、平均值、最大值等,如果用Total、Average、Max这样的限定词来修饰,把限定词放到最后。如:revenueTotal(总收入)、revenueAverage(平均收入)。

优点:

  • 变量名中最重要的部分,即为这一变量赋予主要含义的部分位于最前面,这样可以突出显示,并会被首先阅读到。
  • 避免同时在程序中使用totalRevenue和revenueTotal而产生的歧义。

注意:

Num这个限定词,放在变量名的结束位置表示一个下标,如customerNum(当前客户的序号)。

为了避免Num带来的麻烦,建议用Count或者Total来表示总数,用Id表示序号。如:customerCount(客户的总数),customerId(客户的编号)。

4、统一技术语言

有些技术语言是通用的,业内人士都能理解,我们应该尽量使用这些术语来进行命名。比如:

  • 数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
  • 展示对象:xxxVO,xxx 一般为网页名称。
  • 抽象类命名使用 Abstract 或 Base 开头,异常类命名使用 Exception 结尾
4.1.1.3.2、自明性

在不借助其他辅助手段的情况下,代码本身就能很清晰地表达自身的含义。

  • 代码中使用了设计模式,在命名上可以体现出来,这样阅读者可以很快领会到设计者的意图

    public class XxxProxy;
    public class XxxListener; 
    public class XxxFactory;
    
  • 使用尽量完整的单词组合

    在 JDK 中,表达原子更新的类名为:AtomicReferenceFieldUpdater
    
4.1.1.3.3、可读性

容易理解,语义清晰,不产生歧义。

  • 严禁使用拼音与英文混合的方式,不允许直接使用中文的方式

  • 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚

  • 别害怕长名称,长而具有描述性的名称比短而令人费解的名称好

  • 杜绝不规范的缩写

    反例:AbstractClass"缩写"命名成 AbsClass
    
  • 避免使用任何语言的种族歧视性词语

4.1.1.4、变量名

  • 应该是名词,能够正确地描述业务,有表达力。
  • 如果一个变量名需要注释来补充说明,那么很可能命名是有问题的。

示例一:

  • 反例:

    int d; // 表示过去的天数
    

    上面的命名,我们只能从注释中知道变量的含义。

  • 正例:

    int elapsedTimeInDays;
    

4.1.1.5、方法名

好的方法名能很清晰的解释代码的意图。

  • 方法名是动词,参数名是名词

    比如 HashMap的put方法:

    public V put(K key, V value)
    
  • 空泛的命名没有意义

    例如,processData() 、handle() 就不是一个好的命名,因为所有的方法都是对数据的处理,这样的命名并没有表明要做的事情。

    相比之下,validateUserCredentials()就要好许多。

  • 命名要体现做什么,而不是怎么做

    假如我们将雇员信息存储在一个栈中,现在要从栈中获取最近存储的一个雇员信息,那么getLatestEmployee()就比popRecord()要好,因为栈数据结构是底层实现细节。

4.1.1.6、类名

  • 如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形式)

    // 实现此接口可以使你的类具有迭代遍历能力
    public interface Iterable<T> 
    
  • 基于 SOA 的理念,暴露出来的服务一定是接口,实现类用 Impl 作为后缀

  • 枚举类名建议带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。

  • 领域模型命名:

    • 数据对象:xxxDO,xxx 即为数据表名。
    • 数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
    • 展示对象:xxxVO,xxx 一般为网页名称。
    • POJO 是 DO/DTO/BO/VO 的统称,禁止命名成 xxxPOJO。

4.1.2、方法

4.1.2.1、原则

  • 短小

    超长方法是典型的代码坏味道,对它结构化分解是提升代码可读性的最有效方式之一。

    定量的衡量标准是限制代码的行数。很多资料中建议不超过20行。

  • 单一职责原则(SRP)

    函数应只做一件事,做好一件事。遵循SRP不仅可以提升代码的可读性,还能提升代码的可复用性。

    当发现很难给方法起名字时,需要考虑下是不是因为这个方法要做的事情太多了,导致我们很难用一个词或词组来描述方法的意图。

  • 无副作用

    尤其是一些比较隐蔽的副作用,因为调用方不知道这些隐藏的逻辑,可能会带来风险。

  • 方法参数不要太多

  • 参数越多的方法,调用时越麻烦。尽量保持参数数量足够少,最好是没有,超过3个时最好做下封装。

  • null

    尽量不要返回 null ,尽量不要传 null参数,降低 NPE 的可能性。

  • 条件分支

    过多的 if...else 或者 switch,都应该考虑用多态来替换掉。

4.1.3、注释

1、不要复述功能,要解释背后意图

注释要能够解释代码背后的意图,而不是对功能的简单重复。例如:

try {
    // 在这里等待2秒
    Thread.sleep(2000);
} catch (InterruptedException e) {
}

这里的注释和没写是一样的,因为它只是对sleep的简单复述。更好的做法应该是阐述sleep背后的原因。

try {
    // 休息2秒,为了等待关联系统处理结果
    Thread.sleep(2000);
} catch (InterruptedException e) {
}

2、要考虑下注释是否是必需的,如果代码有足够强的表达力,是不需要写注释的

在JDK的源码 java.util.logging.Handler 中,有如下代码:

public synchronized void setFormatter(Formatter newFormatter){
    checkPermission();
    // Check for a null pointer:
    newFormatter.getClass();
    formatter = newFormatter;
}

如果没有注释,那么可能没人知道 newFormatter.getClass() 是为了判空,注释是为了弥补代码表达能力的失败而存在的。

如果换一种写法,使用java.util.Objects.requireNonNull进行判空,那么注释就完全是多余的,代码本身足以表达其意图。

3、别给糟糕的代码加注释,重构它

注释不能美化糟糕的代码。当企图使用注释前,先考虑是否可以通过调整结构,命名等操作,消除写注释的必要,往往这样做之后注释就多余了。

4、不要滥用注释

过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担;如果代码逻辑与注释没有保持同步,甚至会产生误导。

4.1.4、设计模式

4.1.5、踩坑记录

4.1.6、新版本

JDK版本用的比较多的是8,目前已经发展到了16,17也即将在今年推出。

每个版本的推出都会带来一些新特性、新思想。比如JDK8的 stream,使代码更精简。

历代 JDK 汇总:

.........

4.1.7、第三方库

比如:apache、google等推出的工具库。

4.1.8、更多规范

4.2、高效

木桶效应

木桶的盛水量,并不取决于最高的那块木板,而是取决于最短的木板。

同样的,系统的性能取决于性能最差的组件,我们需要把它们找出来 ,并进行优化 。

4.2.1、性能

4.2.1.1、问题

常见的可能成为系统瓶颈的因素:

  • 磁盘

    磁盘I/O读写速度比内存慢的多,低效率的I/O操作会拖累系统整体性能

  • 网络

    与磁盘I/O类似,网络环境的复杂性、不确定性等

  • CPU

    计算密集型操作,长时间大量占用CPU资源

  • 内存

    内存资源不足,高频率内存交换和扫描

    异常

    异常的捕获和处理是很消耗资源的

  • 数据库

  • 锁竞争

4.2.1.2、指标

常用的评测指标有:

  • 执行时间
  • CPU时间
  • 内存分配
  • 磁盘吞吐量
  • 网络吞吐量
  • 响应时间

4.2.2、优化

不用层有不同的优化策略,如代码优化、jvm调优、架构优化、数据库优化、结合操作系统、硬件等等做优化。

第一层主要是代码优化。

4.2.2.1、Buffer

作用:

  • 可以协调上下游的性能差,提升整体性能
  • 可以作为上下游的通信工具,解耦

应用:

JDK中为很多I/O组件提供了Buffer功能,比如:

@Test
public void testNotUseBuffer() throws IOException {
    Writer writer = new FileWriter(new File("D:\\abc.txt"));
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        writer.write(i);
    }
    writer.close();
    System.out.println(System.currentTimeMillis() - begin); // 7669
}

@Test
public void testUseBuffer() throws IOException {
    // BufferedWriter默认8k缓冲区
    Writer writer = new BufferedWriter(new FileWriter(new File("D:\\abc.txt")));
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        writer.write(i);
    }
    writer.close();
    System.out.println(System.currentTimeMillis() - begin);  //4574
}

buffer的使用使性能提高了将近一倍。

4.2.2.2、Cache

作用:

  • 暂存处理结果,下次可以直接使用,减少资源消耗

应用:

JDK中最简单的缓存组件:HashMap

Cache(缓存) 和 Buffer (缓冲)的区别:

Cache 好比是把常看的书放在书桌上,随手可拿,不用再费劲去柜子里找
Buffer 好比是垃圾桶,垃圾满了再去扔

4.2.2.3、池化

池化技术,可以有效的改善系统在高并发下的性能

最常见的:线程池,数据库连接池、对象池(如 apache的 ObjectPool对象池组件)

4.2.2.4、并发

4.2.2.5、并行

4.2.2.6、负载均衡

4.2.2.7、空间&时间

4.2.3、算法

4.2.4、新版本

JDK版本用的比较多的是8,目前已经发展到了16,17也即将在今年推出。

jdk的源码已经写得很优秀了,但是还在不断的更新升级,背后是一些设计思想的改变和完善。

比如:ConcurrentHashMap,1.7和1.8的设计方案差别很大,带来的是性能的极大提升。