最近在读这本<代码整洁之道>,感觉里面有很多内容都很有启发。整理下来,大家一起看下,顺便看看作者说的有没有道理。这本书的作者是:罗伯特丶马丁,大家经常叫他鲍勃大叔。下面直接进入正题,先看一下目录
文章目录
- 1、有意义的命名
- 1.1、变量的命名
- 1.2、方法的命名
- 1.3、类名
- 2、方法的编写
- 2.1、单一职责原则
- 2.2、方法中的逻辑要属于一个层级
- 2.3、不要把返回值作为方法的入参
- 2.4、方法入参个数
- 2.5、方法入参类型
- 2.6、尽量使用异常,少用错误码
- 2.7、方法中多用return、continue、goto
- 3、注释
- 3.1、好的注释
- 3.1.1、作者的一个重要决策
- 3.1.2、强调逻辑的重要性
- 3.1.3、//todo、//fixme注解的使用
- 3.2、坏的注释
- 3.2.1、含糊不清的注释
- 3.1.2、没有任何用处的注释
- 3.1.3、太长的注释
- 4、错误处理
- 4.1、使用未受检异常
- 4.2、手动抛出的异常要提供足够的上下文
- 4.3、对异常进行分类
- 4.4、特例对象简化调用端逻辑
- 4.5、方法出参不要返回null,方法入参不要传递null
- 5、边界
1、有意义的命名
这一章主要分成三个小部分。
1.1、变量的命名
主要讲究的就是见名知意,不要随便写一个 abc之类的看不懂意图的名称,会给阅读这段代码的同学造成困扰,同时这种abc的名称也不易检索。另外注意,不要用一些具有误导性的词汇。
1.2、方法的命名
方法一般都讲究单一职责,所以我们是可以根据方法的内容来凝练出一个词语或一个词组来表达这个方法的作用
1.3、类名
一般是名词
2、方法的编写
这一张主要分成三个小部分
2.1、单一职责原则
单一职责原则很好的体现了java的封装原则,一个方法,只做一件事。这样做有两个好处,方法的可复用性强,另外,方法很好阅读。
2.2、方法中的逻辑要属于一个层级
这一段内容,我认为是对单一职责原则的补充。一个方法中,是可能会遇到有多段逻辑的情况,但是这些逻辑做的事需要是一个层级的。比如:小品里说的,要把大象装冰箱,总共分几步?三步。这三步,其实都是围绕着大象装冰箱这个事来的。切忌不能在方法中做一些和主逻辑不相干的事。
2.3、不要把返回值作为方法的入参
这一点,我自己是要反思的,我经常会从外面new一个list,或者对象啥的,通过入参传递到方法中,在方法中改动这个对象。这种方式能避免就避免
2.4、方法入参个数
方法入参越少越好,最好没有入参,尽量不要超过3个,超过3个入参,会让方法理解起来很困难。如果超过了3个,最好采用一些封装的手段,将参数封装到一个对象中,然后方法的参数传递这个对象。
比如:原方法签名为:writeField(OutputStream outputStream,name)
这是一个两参数方法,如何减少方法入参个数呢?
我们可以新定义一个FieldWriter类
public class FieldWriter {
private OutputStream outputStream;
public void write(String name){
........
}
}
然后在原来引用writeField方法的地方,替换为fieldWriter.write(name),这样就利用面向对象的思想将outputStream封装到了类中,减少了方法参数的个数
2.5、方法入参类型
尽量不要入参布尔类型,更不要将布尔类型作为方法内部变更逻辑的标志
2.6、尽量使用异常,少用错误码
如果我们使用异常码的话,方法的调用方需要在逻辑中进行一系列的判断。如果方法新增了错误码,调用方很有可能需要跟着改
但如果用异常的话,我们可以把逻辑简化成下面这样
这个点,我觉着值得商榷,书上这样的写法,是不关心异常的写法,如果调用方需要根据方法的返回来做不同的逻辑呢?还是要捕获不同异常,来做不同的逻辑。
我觉着作者想表达的思想就是,让方法的调用者处理起来逻辑要简单。我们不要照抄他的说法,多学习他的思想
2.7、方法中多用return、continue、goto
不要一个逻辑写到底,在需要返回的地方,直接返回就可以了,其他人在阅读这段代码的时候,他就很容易知道,逻辑的终止点,梳理起来要容易
3、注释
按照书中作者的说法,如果我们需要给一个方法加注释,自己应该感到羞耻。加注释就意味着,方法的逻辑复杂,我们担心其他人看不懂,所以需要专门说明一下。我觉着作者的想法是有道理的,但也不是说不能用注释,作者就给列举了几个应该用注释的场景,以及如何写好注释
3.1、好的注释
3.1.1、作者的一个重要决策
比如一段算法的改进,我们可以加一个注释,说明一下我们的设计思路,使用的场景之类,这样读者可以更容易的掌握这段算法
3.1.2、强调逻辑的重要性
我们可能会写一些看似无关紧要的逻辑,如果我们担心其他人改动的时候,影响到这段逻辑,此时可以加一些注释,提醒一下。
3.1.3、//todo、//fixme注解的使用
我们要注意使用这两个特殊的注释,来表明我们将来要做的事,或者目前是一个临时的写法,未来可以改进到更好
3.2、坏的注释
3.2.1、含糊不清的注释
比如下面这段方法,这是一个著名开源项目中的一个代码片段
在捕获到IOException后,源码作者写了一个注释:
//No properties files means all defaults are loaded
针对这段注释,鲍勃大叔发起了一系列追问
1)、谁来加载默认的配置
2)、调用loadedProperties.load方法的时候,默认配置加载完了吗?
3)、捕获到IOException异常后,作者没有做任何的处理,就只写了一个注释,那作者是觉着不用处理,还是后面会继续处理?
这段注释能被人问出这么多问题,就证明注释写的非常失败
3.1.2、没有任何用处的注释
下面的这几个例子,就是纯纯的废话,没有提供任何有用的信息。我们完全可以使用一个有意义的变量来替代注释
3.1.3、太长的注释
下面这个注释,说实话,我是不想读的。太长了
4、错误处理
4.1、使用未受检异常
我们先看一下受检异常,受检异常,如果不捕获,程序编译会失败。我们常见的IOException,就是受检异常。
未受检异常,比如:RuntimeException。不捕获,程序可以正常编译。
使用未受检异常,代码看起来更清爽
4.2、手动抛出的异常要提供足够的上下文
足够的上下文,其实就是我们平时所说的堆栈信息。方法报错,如果能看到完整的堆栈,方法的调用者很快就能定位到问题
4.3、对异常进行分类
未分类前
需要捕获各种异常,看着很乱。
我们可以对异常进行一些分类,如下:
可以看到,我们构建出了一个PortDeviceFailure异常。然后在LocalPort类的open方法中,对各种异常进行了分类,和PortDeviceFailure异常属于一类的异常,都统一抛出PortDeviceFailure异常
4.4、特例对象简化调用端逻辑
我们看下面的一段代码。
如果未捕获到异常,调用MealExpenses的getTotal方法。
如果捕获到MealExpensesNotFound异常,调用getMealPerDiem方法
我们可以考虑使用多态 + 特例的方式简化以上逻辑
可以看到我们将getMealPerDiem方法封装成了PerDiemMealExpenses对象。如果抛出了MealExpensesNotFound异常,我们就返回PerDiemMealExpenses对象,就类似下面的处理方式
public class ExpenseReportDao {
MealExpenses getMeals(String id) {
try {
return new NormalMealExpenses();
} catch (MealExpensesNotFound e) {
return new PerDiemMealExpenses();
}
}
}
这样,调用端的逻辑就不需要在捕获MealExpensesNotFound的异常,直接调用getMeals(String id)方法拿到MealExpenses对象,然后调用其getTotal方法就可以了
MealExpenses mealExpenses = expenseReportDao.getMeals(employee.getID());
m_total += mealExpenses.getTotal();
这样,方法调用者的处理逻辑就简化了很多。
4.5、方法出参不要返回null,方法入参不要传递null
这个很好理解,如果随意返回null或者入参有null,我们就需要判空,众所周知,判空的代码,很丑陋。
public class MetricsCalculator{
public double xProjection(Point p1,Point p2){
if(p1 == null){
throw new IllegalArgumentException("P1 should not be null")
}
if(p2 == null){
throw new IllegalArgumentException("P2 should not be null")
}
......
}
}
考虑使用断言
断言要比传统的判断,形式上更美观一些。
5、边界
边界的理解就是:我们要将方法调用者不关心的部分封装到方法内部,不要让方法的调用者感知到,本质上还是面向对象的思想
看下面的例子
Map sensors = new HashMap();
Sensor s = (Sensor)sensors.get(sensorId);
这样的代码,相信你看到并不会陌生,这是一个强制类型转换的逻辑。
你可能会说,可以给Map加一个Sensor的泛型,这样就可以不用强转了,但是Map里可以放很多东西,不一定只放Sensor类型,如果放了其他类型,那Map的泛型就只能是Object类型,强转不可避免。
此时,我们可以考虑封装一个Sensor类
public class Sensors{
private Map sensors = new HashMap();
public Sensor getById(String id){
return (Sensor)sensors.get(id);
}
...
}
持续更新…