JAVA代码整洁之道

605 阅读13分钟

JAVA代码整洁之道

整洁代码的定义(优雅而高效)

1. 代码逻辑直接了当,让问题难以隐藏

2. 尽量减少依赖关系,以便于维护

3. 确定并统一分层策略,按照规定放置代码, 建议先分层目录,再建立业务文件夹,然后按照分层归属放置文件

4. 将性能调整到最优,让别人不会来优化代码

破窗效应

环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉。 如果代码写的很糟糕,别人在仿照的时候就会更加糟糕。所以代码质量管控需严格且不留意外。

越是复杂逻辑的代码,越需要代码整洁。例如内存泄漏,静态条件代码,这些高并发的场景,更需要严格执行。

优化的思路

1. 应有单元测试和验收测试。

不推荐使用脏测试,随便命名等,因为随着接口后面修改单元测试的代码也需要跟着一起改。

单元测试的结构应为:构造 (构造测试数据)- 操作(执行要测试方法) - 检验(校验测试数据准确性),其中测试数据准确性时应用代码里写Assert判断,而不是通过人工或者日志比对,JUnit中每个测试函数都应该有且只有⼀个断⾔ 语句。

可以使用模板方法模式优化重复代码

一个测试函数只测试一个功能

测试 F.I.R.S.T原则,快速、独立、可重复、自足验证、及时

2. 使用有意义的命名

不要怕命名太长,要一看就能知道他是做什么的, 更便于搜索。

类名用名词修饰,方法名用动词命名,

同时要遵从分层的尾椎(Service,Controller)。

如果用到设计模式要把设计模式的关键词命名到类名里(Factory,Adapter),要尽量体现系统种的设计理念。 IShapeFactory 和 ShapeFactory,可以直接命名为ShapeFactory不用带个I。

同时更要避免错误的命名,误导其他编程人员理解。

拒绝使用单个字母 i,I,1,o,O,0 命名变量,避免单字母命名。

前后不同代码里的相同意义的变量名要尽量保持一致,避免将同一单词用于不同目的

避免带数字的命名方式,如:a1,aN



给命名的类和方法名添加前缀,更能表明他的意义。如AdressXXX

3. 做一件事只提供一种途径, 一个方法只做一件事,

且方法最大不可以超过200行,拆成一个个小的方法,可以先写完,再进行抽取重构。

宽度不要超过120字符,idea有插件能看到对应长度。

方法应该是短小的,一个类里的代码都是自己相关的方法。要检查对象或方法是否想做的事太多。如果一个对象的功能太多,最好是切分成俩个或者多个对象,或者采用抽象的手段重构他

4. 尽量少的依赖关系

5. 提供明确清晰且尽量少的api

6. 代码含义清晰,代码应该在字面上表达其含义。

理清思路后再进行编码,避免条理不清晰或跳跃性的代码

7. 尽量减少重复的代码,抽象,公共提取工具类。拆散类,避免单个类中的内容太多

8. 方法的命名规则要前后保持一致,如果用GetXX就全用这种来命名

9. if、else、while语句,里面的代码块应该只有一行代码,应该是一个函数调用的语句,对应的判断里的逻辑也要尽量用方法封装这样阅读起来更具有意义。例如:if (employee.age > 65) 不如改成 if (employee.isOldPerson())

10. 函数只干一件事,判断函数是否只做了一件事,可以通过看这个函数是不是还能继续拆分成其他的函数

11. 函数的放置位置,应该按照调用关系,从上到下,相互调用的函数应该紧挨着,调用的一方放在上面。

类应该从⼀组变量列表开始。如果有公共 静态常量,应该先出现。然后是私有静态变量,以及私有实体变量。 很少会有公共变量。 公共函数应跟在变量列表之后。

我们喜欢把由某个公共函数调⽤ 的私有⼯具函数紧随在该公共函数后⾯。这符合了⾃顶向下原则。

构造出的类应该尽量短小,同时对外暴露的方法应尽量少,一个类只做一件事,单一全责类,

系统应该由许多短⼩的类⽽不是少量巨⼤的类组 成。每个⼩类封装⼀个权责

12. 参数个数最多不要超过3个,参数越少越是理想。如果一定要超过3个参数可以封装成参照对象

13. 尽量减少出参,如要修改什么,可以在入参中对象修改

14. 不要在方法里传Boolean类型参数,如果需要标识判断逻辑,可以将函数拆成 renderTrue();和 renderFalse();

15. 如果方法传参有多个且传参类型无法分辨出对应的意义,建议在方法名命名的时候按参数顺序的命名。如:writeDataByPathAndFileName(String path,String fileName)

16. 使用异常代替返回错误码,直接throw出异常信息。使⽤异常替代错误码,新异常就可以从异常类派⽣出来,⽆需重 新编译或重新部署。

异常抛出要抛出具体异常。而不是Exception包含,

推荐使用自定义异常,方便后面扩展可以直接继承出新的异常

17. Try/Catch语句里应该只放函数调用的语句,而不是具体代码逻辑。且catch/finally后面不应该跟其他代码内容

18. 不准确的注释比没注释更要命,

尽量减少注释,避免后面维护修改代码,但是没维护注释的情况。

注释这个本就是代码无法被人看懂的无奈之举,真正优秀整洁的代码一眼就可以看出来是干嘛的。

不需要的代码直接删掉,而不是注释掉,现在都有git工具,不要害怕删代码。

更不要把自己的喃喃自语放到注释里。

不用把日志性的注释,如谁谁谁在啥时候改了啥,都有git工具看,不需要写在代码里。

不要把注释放到括号后面 。 如 if (xxx()) // 判断xxx

19. 要保留警告性注释,比如该段代码注意事项。和TODO注释,并定期检查TODO的完成程度及时删掉。

公共Api的注释要写的详细,参考Javadoc,除了公共接口应该标清楚注释信息,其他的方法等应尽量减少无用的注释,避免注释掉之前的代码,避免参数的意义描述,应用命名规范让直接可读

20. 在封包声明、导⼊声明和每个函数之间,都 有空⽩⾏隔开。这条极其简单的规则极⼤地影响到代码的视觉外观。 每个空⽩⾏都是⼀条线索,标识出新的独⽴概念。

相关联的代码应该紧挨着,实体变量应该在类的顶部声明

21. 团队应在编码开始前就统一编码风格,可以开10分钟会讨论在什么地⽅放置括号, 缩进⼏个字符,如何命名类、变量和⽅法。

也可以直接共用一套固定的Idea的编码格式化模板

22. 数据结构方面,更偏向于抽象或接口。不愿意暴露数据细节,更愿意使用抽象形态表示数据。对象暴露行为,而隐藏数据。

23. 方法不可以返回null值,如List 如果返回为空就返回Collections.emptyList()。

如果你在调⽤某个第三⽅API中可能返回null值的⽅法,可以考虑 ⽤新⽅法打包这个⽅法,在新⽅法中抛出异常或返回特例对象

24. 方法入参不要接收null值,可以使用入参校验,如果必要参数为空,抛出异常,或者使用断言

25. 对接第三方API时,如对方尚未完成,可以使用先完成自己的代码,定义好interface后。使用Adapter进行跨接

26. 代码在设计的时候,应该提升内 聚性,降低耦合度,切分关注⾯,模块化系统性关注⾯,缩⼩函数和 类的尺⼨,选⽤更好的名称。消除重复,保证表达⼒,尽可能减少类和⽅法的数量

27. 对象是过程的抽象。线程是调度的抽象。

多线程应该限制数据的作用域。或可以使用数据的复制体,从而避免使用共享数据。

线程应尽可能独立,而不是子线程混杂。

使用生产者消费者模型来避免多线程。

,要保证对应锁的方法代码块尽量小,避免资源争⽤、降低执⾏效率

整理思路遵从的设计原则

1. 单一权责原则

2. 开闭原则

3. 依赖倒置原则

案例1

优化前

public List<int[]> getFlaggedCells() {
    List<int[]> flaggedCells = new ArrayList<int[]>();
    for (int[] cell : gameBoard)
        if (cell[STATUS_VALUE] == FLAGGED)
            flaggedCells.add(cell);
    return flaggedCells;
}

优化后

public List<Cell> getFlaggedCells() {
    List<Cell> flaggedCells = new ArrayList<Cell>();
    for (Cell cell : gameBoard)
        // 将枚举或者常量的对比封装成一个方法更容易理解含义
        if (cell.isFlagged())
            flaggedCells.add(cell);
    return flaggedCells;
}

案例2 - 静态工厂方法命名

优化前

Complex fulcrumPoint = new Complex(23.0);

优化后

// 写成方法,会更有意义,可以考虑将构造器设置成private, 强制使用静态方法命名
Complex fulcrumPoint = Complex.fromRealNumber(23.0);

案例3 - 命名改造

优化前

private void printGuessStatistics(char candidate, 
                                    int count) {
    String number;
    String verb;
    String pluralModifier;
    if (count == 0) {number = "no";
        verb = "are";
        pluralModifier = "s";
    } else if (count == 1) {
        number = "1";
        verb = "is";
        pluralModifier = "";
    } else {
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "s";
    }
    String guessMessage = String.format(
        "There %s %s %s%s", 
        verb, 
        number,
        candidate,
        pluralModifier
    );
    print(guessMessage);
}

优化后

public class GuessStatisticsMessage {
    // 将方法里的传参改成类的属性,再通过其他方式赋值,同时拆分函数
    private String number;
    private String verb;
    private String pluralModifier;
    
    public String make(char candidate, int count) {
        createPluralDependentMessageParts(count);
        return String.format(
            "There %s %s %s%s",
            verb, number, candidate, pluralModifier );
    }
        
    private void createPluralDependentMessageParts(int count) {
        if (count == 0) {
            thereAreNoLetters();
        } else if (count == 1) {
            thereIsOneLetter();
        } else {
            thereAreManyLetters(count);
        }
    }
    
    private void thereAreManyLetters(int count) {
        number = Integer.toString(count);
        verb = "are";
        pluralModifier = "s";
    }
    
    private void thereIsOneLetter() {
        number = "1";
        verb = "is";
        pluralModifier = "";
    }
        
    private void thereAreNoLetters() {
        number = "no";
        verb = "are";
        pluralModifier = "s";
    }
}

案例4 - switch语句优化

sitch既占用大量的代码块,又违反了单一权证原则和违反开放闭合原则,没回添加新类型都需要修改。

可以使用多态,或者工厂来优化他

优化前

public Money calculatePay(Employee e)
                    throws InvalidEmployeeType {
    switch (e.type) {
        case COMMISSIONED:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvalidEmployeeType(e.type);
    }
}

优化后

public abstract class Employee {
    public abstract boolean isPayday();

    public abstract Money calculatePay();

    public abstract void deliverPay(Money pay);
}
-----------------

public interface EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r)
            throws InvalidEmployeeType;
}
-----------------

public class EmployeeFactoryImpl implements
        EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r)
            throws InvalidEmployeeType {
        switch (r.type) {
            case COMMISSIONED:
                return new CommissionedEmployee(r);
            case HOURLY:
                return new HourlyEmployee(r);
            case SALARIED:
                return new SalariedEmploye(r);
            default:
                throw new InvalidEmployeeType(r.type);
        }
    }
}

案例5 - 数据结构抽象出来

对象把数据隐藏 于抽象之后,曝露操作数据的函数。数据结构曝露其数据,没有提供 有意义的函数

得墨忒⽿律认为,类C的⽅法f只应该调⽤以下对象 的⽅法:

C 由

f创建的对象;

作为参数传递给f的对象;

由C的实体变量持有的对象。

⽅法不应调⽤由任何函数返回的对象的⽅法

最为精练的数据结构,是⼀个只有公共变量、没有函数的类。这 种数据结构有时被称为数据传送对象,或DTO

优化前

public class Square {
	public Point topLeft;
	public double side;
}

public class Rectangle {
	public Point topLeft;
	public double height;
	public double width;
}

public class Circle {
	public Point center;
	public double radius;
}

public class Geometry {
	public final double PI = 3.141592653589793;

	public double area(Object shape) throws
			NoSuchShapeException {
		if (shape instanceof Square) {
			Square s = (Square) shape;
			return s.side * s.side;
		} else if (shape instanceof Rectangle) {
			Rectangle r = (Rectangle) shape;
			return r.height * r.width;
		} else if (shape instanceof Circle) {
			Circle c = (Circle) shape;
			return PI * c.radius * c.radius;
		}
		throw new NoSuchShapeException();
	}
}

优化后

// 合理使用private封装数据。对外暴露更多的方法,而不是数据
public class Square implements Shape {
    private Point topLeft;
    private double side;

    public double area() {
        return side * side;
    }
}

public class Rectangle implements Shape {
    private Point topLeft;
    private double height;
    private double width;

    public double area() {
        return height * width;
    }
}

public class Circle implements Shape {
    private Point center;
    private double radius;
    public final double PI = 3.141592653589793;

    public double area() {
        return PI * radius * radius;
    }
}

优化前

// 类似于火车车厢拼接了,违反得墨忒⽿律
final String outputDir =
        ctxt.getOptions().getScratchDir().getAbsolutePath();

优化后

public String getScratchDirectoryOption() {
    Options opts = ctxt.getOptions();
    File scratchDir = opts.getScratchDir();
    final String outputDir = scratchDir.getAbsolutePath();  
    return outputDir;
}

重要事项总结

注释

1. 避免不恰当的信息

2. 删掉废弃的注释

3. 去掉冗余注释

4. 去掉糟糕的注释

5. 删掉注释掉的代码

环境

1. 用单步的小操作构建系统

2. 应当能够发出单个指令就可以运⾏全部单元测试

方法

1. 避免过多的参数

2. 尽量减少出参,如要修改什么,可以在入参中对象修改

3. 尽量避免入参是Boolean的标识

4. 永不调用的方法要直接删除

JAVA

1. 通过使⽤通配符避免过⻓的导⼊清单,指的是import package.*; 这一点与阿里规范相反,个人更喜欢阿里规范

2. 不要继承常量

3. 相同类型的,用枚举来代替常量

名称

1. 采⽤描述性名称,要舍得花更多的时间命名,不要怕长

2. 名称应与抽象层级相符,方法名和类名相关联

3. 尽可能使⽤标准命名法,而不是缩写

4. 避免使用带有歧义或容易混淆的命名

5. 避免命名中带有编码性的前缀,如m_ , f_

6. 方法的命名就能说明该作用,避免同一个方法里做多件事

测试

1. 一定要单元测试,要涵盖场景,避免测试不足

2. 覆盖率⼯具能汇报你测试策略中的缺⼝。使⽤覆盖率⼯具能更容 易地找到测试不⾜的模块、类和函数

3. 特别注意测试边界条件。

4. 全⾯测试相近的缺陷

5. 测试失败的模式有启发性

6. 测试应该快速,优化效率

其他问题

1. 同一份代码文件或文件夹不应存在不同语言的文件

2. 考虑代码安全性,边界问题,线程安全

3. 抽象或抽取工具类,避免重复代码

4. 方法里确定执行不到的代码应该删除

5. 垂直分隔,按规则组织类中的变量,方法等,合理使用换行

6. 避免命名前后不一致

7. 不互相依赖的东⻄不该耦合

8. 用静态方法代替构造函数

9. 变量在命名的时候就要表达出他的意图。Date daysLater = date.add(5);

10. ⽤多态替代If/Else或Switch/Case

11. ⽤命名常量替代魔术数

12. if语句封装条件,例如:if (shouldBeDeleted(timer))

13. 尽可能将条件表⽰为肯定 形式。例如: if (buffer.shouldCompact()) 要好于 if (!buffer.shouldNotCompact())

14. 单个方法只做一件事

15. 几个方法带有执行先后顺序的,尽量用返回值或者抽出单独方法。如:

public class MoogDiver {
    Gradient gradient;
    List<Spline> splines;
    
    public void dive(String reason) {
        Gradient gradient = saturateGradient();
        List<Spline> splines = reticulateSplines(gradient);
        diveForMoog(splines, reason);
        ...
    }
}

16. 应尽量判断入参,返回值 边界条件与null

17. 一个类用到父类的常量,一定要写父类名.常量。